├── .gitignore ├── README.md ├── requirements.txt ├── sdk ├── README.md ├── __init__.py ├── eth_utils │ ├── __init__.py │ ├── assets │ │ ├── erc20.abi │ │ ├── erc20_assets.json │ │ └── exchangeV3.abi │ └── exchange_utils.py ├── ethsnarks │ ├── __init__.py │ ├── cli │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ └── __init__.cpython-37.pyc │ │ ├── proof2sol.py │ │ ├── utils.py │ │ ├── verify.py │ │ └── vk2sol.py │ ├── eddsa.py │ ├── evmasm.py │ ├── field.py │ ├── jubjub.py │ ├── merkletree.py │ ├── mimc │ │ ├── __init__.py │ │ ├── contract.py │ │ ├── contract_sol.py │ │ └── permutation.py │ ├── numbertheory.py │ ├── pedersen.py │ ├── poseidon │ │ ├── __init__.py │ │ ├── contract.py │ │ └── permutation.py │ ├── r1cs.py │ ├── sha3.py │ ├── shamirspoly.py │ ├── utils.py │ └── verifier.py ├── loopring_v3_client.py ├── request_utils │ ├── __init__.py │ └── rest_client.py └── sig_utils │ ├── __init__.py │ ├── ecdsa_utils.py │ ├── eddsa_utils.py │ └── hex_utils.py ├── test ├── __init__.py ├── exchange_utils │ └── test_exchange.py └── v3explorer │ ├── test_ecdsa.py │ └── test_eddsa.py ├── tutorials ├── README.md ├── hash_and_sign │ ├── README.md │ ├── __init__.py │ └── poseidon_hash_sample.py └── transfer │ ├── README.md │ └── transfer_tutorial.py └── v3explorer ├── README.md ├── __init__.py └── api_explorer.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.*~ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hello Loopring 2 | 3 | Loopring V3 api sample with sdk 4 | 5 | ## Install 6 | 7 | ```bash 8 | $pip install -r requirements.txt # install lib dependencies 9 | $export PYTHONPATH=${PWD} # use local ethsnarks to avoid conflicts 10 | ``` 11 | 12 | ## Directory Contents 13 | 14 | ```she 15 | . 16 | ├── sdk: sdk utils, include poseidon hash, both ecdsa/eddsa signing and a workable loopringV3 sample client with full Loopring L2 functions. 17 | ├── test: ecdsa & eddsa signing tests, as helper to debug signature issues. 18 | ├── tutorials: hash/signing code example, and a step-by-step tutorial of loopring transfer. 19 | └── v3explorer: client sample with full DEX functions. 20 | ``` 21 | 22 | ## Getting Started 23 | 24 | Use tutorials to get familiar with how to do sign a request, and how to make a L2 offchain request in Loopring DEX. Go to [Tutorial](https://github.com/Loopring/hello_loopring/tree/loopring-v3/tutorials) for more details 25 | 26 | ## A Full Function Client 27 | 28 | There is a full fucntion client sample in v3explorer, user can test all L2 requests by that. Refer to [V3explorer](https://github.com/Loopring/hello_loopring/tree/loopring-v3/v3explorer) directory for more details. 29 | 30 | ## Contacts 31 | 32 | - [exchange@loopring.io](mailto:exchange@loopring.io) 33 | - [Loopring Discord](https://discord.gg/KkYccYp) 34 | 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bitstring 2 | pysha3 3 | pyblake2 4 | eip712-structs 5 | py_eth_sig_utils 6 | web3 7 | ujson 8 | -------------------------------------------------------------------------------- /sdk/README.md: -------------------------------------------------------------------------------- 1 | # Loopring SDK 2 | 3 | Loopring V3 api sdk 4 | 5 | ## Directory Contents 6 | 7 | ```text 8 | . 9 | ├── eth_utils : Ethereum utils for Loopring DEX contract interactions, not really used here. 10 | ├── ethsnarks : zkRollup library, provides poseidon hash algo and eddsa signature. 11 | ├── sig_utils : Request specific hash and signature utilities. 12 | ├── request_utils : Python web requests wrapper. 13 | └── loopring_v3_client.py: a workable full function loopring v3 client. 14 | ``` 15 | 16 | ## Getting Started 17 | Refer to v3explorer to see how this client works -------------------------------------------------------------------------------- /sdk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/sdk/__init__.py -------------------------------------------------------------------------------- /sdk/eth_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/sdk/eth_utils/__init__.py -------------------------------------------------------------------------------- /sdk/eth_utils/assets/erc20.abi: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": false, 4 | "inputs": [ 5 | { 6 | "name": "spender", 7 | "type": "address" 8 | }, 9 | { 10 | "name": "value", 11 | "type": "uint256" 12 | } 13 | ], 14 | "name": "approve", 15 | "outputs": [ 16 | { 17 | "name": "", 18 | "type": "bool" 19 | } 20 | ], 21 | "payable": false, 22 | "stateMutability": "nonpayable", 23 | "type": "function" 24 | }, 25 | { 26 | "constant": true, 27 | "inputs": [], 28 | "name": "totalSupply", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "uint256" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "view", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "decimals", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint8" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": false, 82 | "inputs": [ 83 | { 84 | "name": "spender", 85 | "type": "address" 86 | }, 87 | { 88 | "name": "addedValue", 89 | "type": "uint256" 90 | } 91 | ], 92 | "name": "increaseAllowance", 93 | "outputs": [ 94 | { 95 | "name": "", 96 | "type": "bool" 97 | } 98 | ], 99 | "payable": false, 100 | "stateMutability": "nonpayable", 101 | "type": "function" 102 | }, 103 | { 104 | "constant": true, 105 | "inputs": [ 106 | { 107 | "name": "owner", 108 | "type": "address" 109 | } 110 | ], 111 | "name": "balanceOf", 112 | "outputs": [ 113 | { 114 | "name": "", 115 | "type": "uint256" 116 | } 117 | ], 118 | "payable": false, 119 | "stateMutability": "view", 120 | "type": "function" 121 | }, 122 | { 123 | "constant": false, 124 | "inputs": [ 125 | { 126 | "name": "spender", 127 | "type": "address" 128 | }, 129 | { 130 | "name": "subtractedValue", 131 | "type": "uint256" 132 | } 133 | ], 134 | "name": "decreaseAllowance", 135 | "outputs": [ 136 | { 137 | "name": "", 138 | "type": "bool" 139 | } 140 | ], 141 | "payable": false, 142 | "stateMutability": "nonpayable", 143 | "type": "function" 144 | }, 145 | { 146 | "constant": false, 147 | "inputs": [ 148 | { 149 | "name": "to", 150 | "type": "address" 151 | }, 152 | { 153 | "name": "value", 154 | "type": "uint256" 155 | } 156 | ], 157 | "name": "transfer", 158 | "outputs": [ 159 | { 160 | "name": "", 161 | "type": "bool" 162 | } 163 | ], 164 | "payable": false, 165 | "stateMutability": "nonpayable", 166 | "type": "function" 167 | }, 168 | { 169 | "constant": true, 170 | "inputs": [ 171 | { 172 | "name": "owner", 173 | "type": "address" 174 | }, 175 | { 176 | "name": "spender", 177 | "type": "address" 178 | } 179 | ], 180 | "name": "allowance", 181 | "outputs": [ 182 | { 183 | "name": "", 184 | "type": "uint256" 185 | } 186 | ], 187 | "payable": false, 188 | "stateMutability": "view", 189 | "type": "function" 190 | }, 191 | { 192 | "anonymous": false, 193 | "inputs": [ 194 | { 195 | "indexed": true, 196 | "name": "from", 197 | "type": "address" 198 | }, 199 | { 200 | "indexed": true, 201 | "name": "to", 202 | "type": "address" 203 | }, 204 | { 205 | "indexed": false, 206 | "name": "value", 207 | "type": "uint256" 208 | } 209 | ], 210 | "name": "Transfer", 211 | "type": "event" 212 | }, 213 | { 214 | "anonymous": false, 215 | "inputs": [ 216 | { 217 | "indexed": true, 218 | "name": "owner", 219 | "type": "address" 220 | }, 221 | { 222 | "indexed": true, 223 | "name": "spender", 224 | "type": "address" 225 | }, 226 | { 227 | "indexed": false, 228 | "name": "value", 229 | "type": "uint256" 230 | } 231 | ], 232 | "name": "Approval", 233 | "type": "event" 234 | } 235 | ] -------------------------------------------------------------------------------- /sdk/eth_utils/assets/erc20_assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "mainnet": { 3 | }, 4 | "goerli": { 5 | "ETH" : "0x0000000000000000000000000000000000000000", 6 | "LRC" : "0xFc28028D9b1F6966Fe74710653232972F50673BE", 7 | "USDT" : "0xd4E71C4bb48850f5971cE40Aa428b09F242D3e8a", 8 | "DAI" : "0xCd2c81B322A5b530b5fA3432E57Da6803b0317f7" 9 | } 10 | } -------------------------------------------------------------------------------- /sdk/eth_utils/exchange_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | from web3 import Web3 5 | 6 | 7 | class EthExchangeWrapper: 8 | def __init__(self, address, private_key, exchange_address, testnet, provider=None, web3=None): 9 | 10 | if not web3: 11 | # Initialize web3. Extra provider for testing. 12 | if not provider: 13 | self.provider = os.environ["PROVIDER"] 14 | self.network = testnet 15 | else: 16 | self.provider = provider 17 | self.network = testnet 18 | if self.provider.startswith('http'): 19 | self.w3 = Web3(Web3.HTTPProvider(self.provider, request_kwargs={"timeout": 60})) 20 | elif self.provider.startswith('ws'): 21 | self.w3 = Web3(Web3.WebsocketProvider(self.provider)) 22 | else: 23 | raise AttributeError(f"Unknown provider {provider}") 24 | else: 25 | self.w3 = web3 26 | self.network = "mainnet" 27 | self.address = Web3.toChecksumAddress(address) 28 | self.private_key = private_key 29 | 30 | # This code automatically approves you for trading on the exchange. 31 | # max_approval is to allow the contract to exchange on your behalf. 32 | # max_approval_check checks that current approval is above a reasonable number 33 | # The program cannot check for max_approval each time because it decreases 34 | # with each trade. 35 | self.eth_address = "0x0000000000000000000000000000000000000000" 36 | self.max_approval_hex = "0x" + "f" * 64 37 | self.max_approval_int = int(self.max_approval_hex, 16) 38 | self.max_approval_check_hex = "0x" + "0" * 15 + "f" * 49 39 | self.max_approval_check_int = int(self.max_approval_check_hex, 16) 40 | 41 | # Initialize address and contract 42 | path = f"{os.path.dirname(os.path.abspath(__file__))}/assets/" 43 | with open(os.path.abspath(path + "erc20_assets.json")) as f: 44 | token_addresses = json.load(f)[self.network] 45 | with open(os.path.abspath(path + "exchangeV3.abi")) as f: 46 | exchange_abi = json.load(f) 47 | with open(os.path.abspath(path + "erc20.abi")) as f: 48 | erc20_abi = json.load(f) 49 | 50 | # Define exchange address, contract instance, and token_instance based on 51 | # token address 52 | 53 | self.exchange_contract = self.w3.eth.contract( 54 | address=Web3.toChecksumAddress(exchange_address), abi=exchange_abi 55 | ) 56 | self.deposit_contract = self.getDepositContract() 57 | 58 | self.erc20_contract = {} 59 | for token_name, token_address in token_addresses.items(): 60 | if token_address != self.eth_address: 61 | self.erc20_contract[token_name] = self.w3.eth.contract( 62 | address= Web3.toChecksumAddress(token_address), abi=erc20_abi 63 | ) 64 | 65 | def getDomainSeparator(self): 66 | exchange_funcs = self.exchange_contract.functions 67 | return exchange_funcs.getDomainSeparator().call() 68 | 69 | def getDepositContract(self): 70 | exchange_funcs = self.exchange_contract.functions 71 | return exchange_funcs.getDepositContract().call() 72 | 73 | # ------ Decorators ---------------------------------------------------------------- 74 | def check_approval(method): 75 | """Decorator to check if user is approved for a token. It approves them if they 76 | need to be approved.""" 77 | 78 | def approved(self, *args): 79 | # Check to see if the first token is actually ETH 80 | token = args[0] 81 | token_contract = self.erc20_contract.get(token, None) 82 | amount = int(args[1]*1e18) 83 | 84 | # Approve both tokens, if needed 85 | if token_contract: 86 | is_approved = self._is_approved(token) 87 | if not is_approved: 88 | self.approve_erc20_transfer(token, amount) 89 | 90 | return method(self, *args) 91 | 92 | return approved 93 | 94 | # ------ Funcs ---------------------------------------------------------------- 95 | @check_approval 96 | def deposit(self, token, amount): 97 | exchange_funcs = self.exchange_contract.functions 98 | token_address = self.eth_address 99 | eth_qty = int(amount * 1e18) 100 | token_qty = 0 101 | if token in self.erc20_contract: 102 | token_address = self.erc20_contract[token].address 103 | token_qty = int(amount * 1e18) 104 | eth_qty= 0 105 | tx_params = self._get_tx_params(value=eth_qty, gas=500000) 106 | func_params = [self.address, self.address, token_address, token_qty, bytes(0)] 107 | function = exchange_funcs.deposit(*func_params) 108 | return self._build_and_send_tx(function, tx_params) 109 | 110 | def approveTransaction(self, txHash): 111 | exchange_funcs = self.exchange_contract.functions 112 | tx_params = self._get_tx_params(value=0, gas=300000) 113 | func_params = [self.address, txHash] 114 | function = exchange_funcs.approveTransaction(*func_params) 115 | return self._build_and_send_tx(function, tx_params) 116 | 117 | # ------ Approval Utils ------------------------------------------------------------ 118 | def approve_erc20_transfer(self, token, max_approval=None): 119 | """Give an exchange max approval of a token.""" 120 | max_approval = self.max_approval_int if not max_approval else max_approval 121 | tx_params = self._get_tx_params() 122 | exchange_addr = self.exchange_contract.address 123 | function = self.erc20_contract[token].functions.approve( 124 | self.deposit_contract, max_approval 125 | ) 126 | tx = self._build_and_send_tx(function, tx_params) 127 | self.w3.eth.waitForTransactionReceipt(tx, timeout=6000) 128 | # Add extra sleep to let tx propogate correctly 129 | time.sleep(1) 130 | 131 | def _is_approved(self, token): 132 | """Check to see if the exchange and token is approved.""" 133 | exchange_addr = self.exchange_contract.address 134 | amount = ( 135 | self.erc20_contract[token].functions.allowance(self.address, exchange_addr).call() 136 | ) 137 | if amount >= self.max_approval_check_int: 138 | return True 139 | else: 140 | return False 141 | 142 | # ------ Tx Utils ------------------------------------------------------------------ 143 | def _deadline(self): 144 | """Get a predefined deadline.""" 145 | return int(time.time()) + 1000 146 | 147 | def _build_and_send_tx(self, function, tx_params): 148 | """Build and send a transaction.""" 149 | transaction = function.buildTransaction(tx_params) 150 | signed_txn = self.w3.eth.account.signTransaction( 151 | transaction, private_key=self.private_key 152 | ) 153 | return self.w3.eth.sendRawTransaction(signed_txn.rawTransaction) 154 | 155 | def _get_tx_params(self, value=0, gas=150000): 156 | """Get generic transaction parameters.""" 157 | return { 158 | "from": self.address, 159 | "value": value, 160 | "gas": gas, 161 | "nonce": self.w3.eth.getTransactionCount(self.address), 162 | } 163 | -------------------------------------------------------------------------------- /sdk/ethsnarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/sdk/ethsnarks/__init__.py -------------------------------------------------------------------------------- /sdk/ethsnarks/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/sdk/ethsnarks/cli/__init__.py -------------------------------------------------------------------------------- /sdk/ethsnarks/cli/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/sdk/ethsnarks/cli/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /sdk/ethsnarks/cli/proof2sol.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | 4 | from ..verifier import Proof 5 | 6 | from .utils import g2_to_sol, g1_to_sol 7 | 8 | 9 | def main(vk_filename, name='_getStaticProof'): 10 | """Outputs the solidity code necessary to instansiate a ProofWithInput variable""" 11 | with open(vk_filename, 'r') as handle: 12 | proof = Proof.from_dict(json.load(handle)) 13 | 14 | out = [ 15 | "\tfunction %s (Verifier.ProofWithInput memory output)" % (name), 16 | "\t\tinternal pure", 17 | "\t{", 18 | "\t\tVerifier.Proof memory proof = output.proof;" 19 | ] 20 | 21 | for k in proof.G2_POINTS: 22 | x = getattr(proof, k) 23 | out.append("\t\tproof.%s = %s;" % (k, g2_to_sol(x))) 24 | 25 | for k in proof.G1_POINTS: 26 | x = getattr(proof, k) 27 | out.append("\t\tproof.%s = %s;" % (k, g1_to_sol(x))) 28 | 29 | out.append("\t\toutput.input = new uint256[](%d);" % (len(proof.input),)) 30 | for i, v in enumerate(proof.input): 31 | out.append("\t\toutput.input[%d] = %s;" % (i, hex(v))) 32 | 33 | out.append("\t}") 34 | print('\n'.join(out)) 35 | 36 | 37 | if __name__ == "__main__": 38 | if len(sys.argv) < 2: 39 | print("Usage: ethsnarks.cli.proof2sol [func-name]") 40 | print("Outputs Solidity code, depending on Verifier.sol, which can be included in your code") 41 | sys.exit(1) 42 | sys.exit(main(*sys.argv[1:])) 43 | -------------------------------------------------------------------------------- /sdk/ethsnarks/cli/utils.py: -------------------------------------------------------------------------------- 1 | 2 | def fq_to_sol(o): 3 | return '%s' % (hex(o.n),) 4 | 5 | 6 | def fq2_to_sol(o): 7 | # Fq2 is big-endian in EVM, so '[c1, c0]' 8 | return '[%s, %s]' % (fq_to_sol(o.coeffs[1]), fq_to_sol(o.coeffs[0])) 9 | 10 | 11 | def g2_to_sol(o): 12 | return 'Pairing.G2Point(%s, %s)' % (fq2_to_sol(o[0]), fq2_to_sol(o[1])) 13 | 14 | 15 | def g1_to_sol(o): 16 | return 'Pairing.G1Point(%s, %s)' % (fq_to_sol(o[0]), fq_to_sol(o[1])) 17 | -------------------------------------------------------------------------------- /sdk/ethsnarks/cli/verify.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | 4 | from ..verifier import VerifyingKey, Proof 5 | 6 | 7 | def main(vk_file, proof_file): 8 | """Verifies the proof.json using the vk.json""" 9 | with open(vk_file, 'r') as vk_handle: 10 | vk = VerifyingKey.from_dict(json.load(vk_handle)) 11 | with open(proof_file, 'r') as proof_handle: 12 | proof = Proof.from_dict(json.load(proof_handle)) 13 | if not vk.verify(proof): 14 | print("FAIL") 15 | return 1 16 | print("OK") 17 | return 0 18 | 19 | 20 | if __name__ == "__main__": 21 | if len(sys.argv) < 3: 22 | print("Usage: ethsnarks.cli.verify ") 23 | sys.exit(1) 24 | sys.exit(main(*sys.argv[1:])) 25 | -------------------------------------------------------------------------------- /sdk/ethsnarks/cli/vk2sol.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | 4 | from ..verifier import VerifyingKey 5 | 6 | from .utils import g2_to_sol, g1_to_sol 7 | 8 | 9 | def main(vk_filename, name='_getVerifyingKey'): 10 | """Outputs the solidity code necessary to instansiate a VerifyingKey variable""" 11 | with open(vk_filename, 'r') as handle: 12 | vk = VerifyingKey.from_dict(json.load(handle)) 13 | indent = "\t\t" 14 | varname = "vk" 15 | out = [ 16 | "\tfunction %s (Verifier.VerifyingKey memory %s)" % (name, varname), 17 | "\t\tinternal pure", 18 | "\t{", 19 | ] 20 | for k in vk.G2_POINTS: 21 | x = getattr(vk, k) 22 | out.append("%s%s.%s = %s;" % (indent, varname, k, g2_to_sol(x))) 23 | for k in vk.G1_POINTS: 24 | x = getattr(vk, k) 25 | out.append("%s%s.%s = %s;" % (indent, varname, k, g1_to_sol(x))) 26 | 27 | out.append("%s%s.gammaABC = new Pairing.G1Point[](%d);" % (indent, varname, len(vk.gammaABC))) 28 | for i, v in enumerate(vk.gammaABC): 29 | out.append("%s%s.gammaABC[%d] = %s;" % (indent, varname, i, g1_to_sol(v))) 30 | out.append("\t}") 31 | 32 | print('\n'.join(out)) 33 | 34 | 35 | if __name__ == "__main__": 36 | if len(sys.argv) < 2: 37 | print("Usage: ethsnarks.cli.vk2sol [func-name]") 38 | print("Outputs Solidity code, depending on Verifier.sol, which can be included in your code") 39 | sys.exit(1) 40 | sys.exit(main(*sys.argv[1:])) 41 | -------------------------------------------------------------------------------- /sdk/ethsnarks/eddsa.py: -------------------------------------------------------------------------------- 1 | import math 2 | import bitstring 3 | from collections import namedtuple 4 | from hashlib import sha512 5 | 6 | from .field import FQ, SNARK_SCALAR_FIELD 7 | from .jubjub import Point, JUBJUB_L, JUBJUB_Q, JUBJUB_E 8 | from .pedersen import pedersen_hash_bytes, pedersen_hash_bits 9 | from .poseidon import poseidon_params, poseidon 10 | from .mimc import mimc_hash 11 | 12 | 13 | """ 14 | Implements Pure-EdDSA and Hash-EdDSA 15 | 16 | The signer has two secret values: 17 | 18 | * k = Secret key 19 | * r = Per-(message,key) nonce 20 | 21 | The signer provides a signature consiting of two values: 22 | 23 | * R = Point, image of `r*B` 24 | * s = Image of `r + (k*t)` 25 | 26 | The signer provides the verifier with their public key: 27 | 28 | * A = k*B 29 | 30 | Both the verifier and the signer calculate the common reference string: 31 | 32 | * t = H(R, A, M) 33 | 34 | The nonce `r` is secret, and protects the value `s` from revealing the 35 | signers secret key. 36 | 37 | For Hash-EdDSA, the message `M` is compressed before H(R,A,M) 38 | 39 | For further information see: https://ed2519.cr.yp.to/eddsa-20150704.pdf 40 | """ 41 | 42 | 43 | P13N_EDDSA_VERIFY_M = 'EdDSA_Verify.M' 44 | P13N_EDDSA_VERIFY_RAM = 'EdDSA_Verify.RAM' 45 | 46 | 47 | class Signature(object): 48 | __slots__ = ('R', 's') 49 | def __init__(self, R, s): 50 | self.R = R if isinstance(R, Point) else Point(*R) 51 | self.s = s if isinstance(s, FQ) else FQ(s) 52 | assert self.s.m == JUBJUB_Q 53 | 54 | def __iter__(self): 55 | return iter([self.R, self.s]) 56 | 57 | def __str__(self): 58 | return ' '.join(str(_) for _ in [self.R.x, self.R.y, self.s]) 59 | 60 | 61 | class SignedMessage(namedtuple('_SignedMessage', ('A', 'sig', 'msg'))): 62 | def __str__(self): 63 | return ' '.join(str(_) for _ in [self.A, self.sig, self.msg]) 64 | 65 | 66 | class _SignatureScheme(object): 67 | @classmethod 68 | def to_bytes(cls, *args): 69 | # TODO: move to ethsnarks.utils ? 70 | result = b'' 71 | for M in args: 72 | if isinstance(M, Point): 73 | result += M.x.to_bytes('little') 74 | result += M.y.to_bytes('little') 75 | elif isinstance(M, FQ): 76 | result += M.to_bytes('little') 77 | elif isinstance(M, (list, tuple)): 78 | # Note: (list,tuple) must go *below* other class types to avoid type confusion 79 | result += b''.join(cls.to_bytes(_) for _ in M) 80 | elif isinstance(M, int): 81 | result += M.to_bytes(32, 'little') 82 | elif isinstance(M, bitstring.BitArray): 83 | result += M.tobytes() 84 | elif isinstance(M, bytes): 85 | result += M 86 | else: 87 | raise TypeError("Bad type for M: " + str(type(M))) 88 | return result 89 | 90 | @classmethod 91 | def to_bits(cls, *args): 92 | # TODO: move to ethsnarks.utils ? 93 | result = bitstring.BitArray() 94 | for M in args: 95 | if isinstance(M, Point): 96 | result.append(M.x.bits()) 97 | elif isinstance(M, FQ): 98 | result.append(M.bits()) 99 | elif isinstance(M, (list, tuple)): 100 | # Note: (list,tuple) must go *below* other class types to avoid type confusion 101 | for _ in cls.to_bits(M): 102 | result.append(_) 103 | elif isinstance(M, bytes): 104 | result.append(M) 105 | elif isinstance(M, bitstring.BitArray): 106 | result.append(M) 107 | else: 108 | raise TypeError("Bad type for M: " + str(type(M))) 109 | return result 110 | 111 | @classmethod 112 | def prehash_message(cls, M): 113 | """ 114 | Identity function for message 115 | 116 | Can be used to truncate the message before hashing it 117 | as part of the public parameters. 118 | """ 119 | return M 120 | 121 | @classmethod 122 | def hash_public(cls, R, A, M): 123 | """ 124 | Identity function for public parameters: 125 | 126 | R, A, M 127 | 128 | Is used to multiply the resulting point 129 | """ 130 | raise NotImplementedError() 131 | 132 | @classmethod 133 | def hash_secret(cls, k, *args): 134 | """ 135 | Hash the key and message to create `r`, the blinding factor for this signature. 136 | 137 | If the same `r` value is used more than once, the key for the signature is revealed. 138 | 139 | From: https://eprint.iacr.org/2015/677.pdf (EdDSA for more curves) 140 | 141 | Page 3: 142 | 143 | (Implementation detail: To save time in the computation of `rB`, the signer 144 | can replace `r` with `r mod L` before computing `rB`.) 145 | """ 146 | assert isinstance(k, FQ) 147 | data = b''.join(cls.to_bytes(_) for _ in (k,) + args) 148 | return int.from_bytes(sha512(data).digest(), 'little') % JUBJUB_L 149 | 150 | @classmethod 151 | def B(cls): 152 | return Point.generator() 153 | 154 | @classmethod 155 | def random_keypair(cls, B=None): 156 | B = B or cls.B() 157 | k = FQ.random(JUBJUB_L) 158 | A = B * k 159 | return k, A 160 | 161 | @classmethod 162 | def sign(cls, msg, key, B=None): 163 | if not isinstance(key, FQ): 164 | raise TypeError("Invalid type for parameter k") 165 | # Strict parsing ensures key is in the prime-order group 166 | if key.n >= JUBJUB_L or key.n <= 0: 167 | raise RuntimeError("Strict parsing of k failed") 168 | 169 | B = B or cls.B() 170 | A = B * key # A = kB 171 | 172 | M = cls.prehash_message(msg) 173 | r = cls.hash_secret(key, M) # r = H(k,M) mod L 174 | R = B * r # R = rB 175 | 176 | t = cls.hash_public(R, A, M) # Bind the message to the nonce, public key and message 177 | S = (r + (key.n*t)) % JUBJUB_E # r + (H(R,A,M) * k) 178 | 179 | return SignedMessage(A, Signature(R, S), msg) 180 | 181 | @classmethod 182 | def verify(cls, A, sig, msg, B=None): 183 | if not isinstance(A, Point): 184 | A = Point(*A) 185 | 186 | if not isinstance(sig, Signature): 187 | sig = Signature(*sig) 188 | 189 | R, S = sig 190 | B = B or cls.B() 191 | lhs = B * S 192 | 193 | M = cls.prehash_message(msg) 194 | rhs = R + (A * cls.hash_public(R, A, M)) 195 | return lhs == rhs 196 | 197 | 198 | class PureEdDSA(_SignatureScheme): 199 | @classmethod 200 | def hash_public(cls, *args, p13n=P13N_EDDSA_VERIFY_RAM): 201 | return pedersen_hash_bits(p13n, cls.to_bits(*args)).x.n 202 | 203 | 204 | class EdDSA(PureEdDSA): 205 | @classmethod 206 | def prehash_message(cls, M, p13n=P13N_EDDSA_VERIFY_M): 207 | return pedersen_hash_bytes(p13n, M) 208 | 209 | 210 | # Convert arguments to integers / scalar values 211 | # TODO: move to ethsnarks.utils ? 212 | def as_scalar(*args): 213 | for x in args: 214 | if isinstance(x, FQ): 215 | yield int(x) 216 | elif isinstance(x, int): 217 | yield x 218 | elif isinstance(x, Point): 219 | yield int(x.x) 220 | yield int(x.y) 221 | elif isinstance(x, (tuple, list)): 222 | # Note: (tuple,list) must go below other class types, to avoid possible type confusion 223 | for _ in as_scalar(*x): 224 | yield _ 225 | else: 226 | raise TypeError("Unknown type " + str(type(x))) 227 | 228 | 229 | class MiMCEdDSA(_SignatureScheme): 230 | @classmethod 231 | def hash_public(cls, *args, p13n=P13N_EDDSA_VERIFY_RAM): 232 | return mimc_hash(list(as_scalar(*args)), seed=p13n) 233 | 234 | class PoseidonEdDSA(_SignatureScheme): 235 | @classmethod 236 | def hash_public(cls, *args): 237 | PoseidonHashParams = poseidon_params(SNARK_SCALAR_FIELD, 6, 6, 52, b'poseidon', 5, security_target=128) 238 | inputMsg = list(as_scalar(*args)) 239 | return poseidon(inputMsg, PoseidonHashParams) -------------------------------------------------------------------------------- /sdk/ethsnarks/evmasm.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Jordi Baylina 2 | # Copyright (c) 2019 Harry Roberts 3 | # License: LGPL-3.0+ 4 | # 5 | # Based on: https://github.com/iden3/circomlib/blob/master/src/evmasm.js 6 | 7 | from binascii import unhexlify 8 | from collections import defaultdict 9 | 10 | class _opcode(object): 11 | extra = None 12 | def __init__(self, code, extra=None): 13 | self._code = code 14 | if not isinstance(extra, (bytes, bytearray)): 15 | assert extra is None 16 | self.extra = extra 17 | 18 | def data(self): 19 | extra = self.extra if self.extra is not None else b'' 20 | return bytes([self._code]) + extra 21 | 22 | def __call__(self): 23 | return self 24 | 25 | class LABEL(_opcode): 26 | _code = 0x5b 27 | def __init__(self, name): 28 | assert isinstance(name, (str, bytes, bytearray)) 29 | self.name = name 30 | 31 | def _encode_offset(offset): 32 | return bytes([offset >> 16, (offset >> 8) & 0xFF, offset & 0xFF]) 33 | 34 | class PUSHLABEL(_opcode): 35 | def __init__(self, target): 36 | self.target = target 37 | 38 | def data(self, offset): 39 | assert offset >= 0 and offset < (1<<24) 40 | return bytes([0x62]) + _encode_offset(offset) 41 | 42 | class JMP(PUSHLABEL): 43 | _code = 0x56 44 | 45 | def __init__(self, target=None): 46 | super(JMP, self).__init__(target) 47 | 48 | def data(self, offset=None): 49 | if offset is not None: 50 | return super(JMP, self).data(offset) + bytes([self._code]) 51 | return bytes([self._code]) 52 | 53 | class JMPI(JMP): 54 | _code = 0x57 55 | 56 | def DUP(n): 57 | if n < 0 or n >= 16: 58 | raise ValueError("DUP must be 0 to 16") 59 | return _opcode(0x80 + n) 60 | 61 | def SWAP(n): 62 | if n < 0 or n >= 16: 63 | raise ValueError("SWAP must be 0 to 16") 64 | return _opcode(0x8f + n) 65 | 66 | def PUSH(data): 67 | if isinstance(data, int): 68 | if data < 0 or data >= ((1<<256)-1): 69 | raise ValueError("Push value out of range: %r" % (data,)) 70 | hexdata = hex(data)[2:] 71 | if (len(hexdata) % 2) != 0: 72 | hexdata = '0' + hexdata 73 | data = unhexlify(hexdata) 74 | assert isinstance(data, (bytes, bytearray)) 75 | return _opcode(0x5F + len(data), data) 76 | 77 | STOP = _opcode(0x00) 78 | ADD = _opcode(0x01) 79 | MUL = _opcode(0x02) 80 | SUB = _opcode(0x03) 81 | DIV = _opcode(0x04) 82 | SDIV = _opcode(0x05) 83 | MOD = _opcode(0x06) 84 | SMOD = _opcode(0x07) 85 | ADDMOD = _opcode(0x08) 86 | MULMOD = _opcode(0x09) 87 | 88 | EXP = _opcode(0x0a) 89 | SIGNEXTEND = _opcode(0x0b) 90 | LT = _opcode(0x10) 91 | GT = _opcode(0x11) 92 | SLT = _opcode(0x12) 93 | SGT = _opcode(0x13) 94 | EQ = _opcode(0x14) 95 | ISZERO = _opcode(0x15) 96 | AND = _opcode(0x16) 97 | OR = _opcode(0x17) 98 | SHOR = _opcode(0x18) 99 | NOT = _opcode(0x19) 100 | BYTE = _opcode(0x1a) 101 | KECCAK = _opcode(0x20) 102 | SHA3 = _opcode(0x20) 103 | 104 | ADDRESS = _opcode(0x30) 105 | BALANCE = _opcode(0x31) 106 | ORIGIN = _opcode(0x32) 107 | CALLER = _opcode(0x33) 108 | CALLVALUE = _opcode(0x34) 109 | CALLDATALOAD = _opcode(0x35) 110 | CALLDATASIZE = _opcode(0x36) 111 | CALLDATACOPY = _opcode(0x37) 112 | CODESIZE = _opcode(0x38) 113 | CODECOPY = _opcode(0x39) 114 | GASPRICE = _opcode(0x3a) 115 | EXTCODESIZE = _opcode(0x3b) 116 | EXTCODECOPY = _opcode(0x3c) 117 | RETURNDATASIZE = _opcode(0x3d) 118 | RETURNDATACOPY = _opcode(0x3e) 119 | 120 | BLOCKHASH = _opcode(0x40) 121 | COINBASE = _opcode(0x41) 122 | TIMESTAMP = _opcode(0x42) 123 | NUMBER = _opcode(0x43) 124 | DIFFICULTY = _opcode(0x44) 125 | GASLIMIT = _opcode(0x45) 126 | 127 | POP = _opcode(0x50) 128 | MLOAD = _opcode(0x51) 129 | MSTORE = _opcode(0x52) 130 | MSTORE8 = _opcode(0x53) 131 | SLOAD = _opcode(0x54) 132 | SSTORE = _opcode(0x55) 133 | PC = _opcode(0x58) 134 | MSIZE = _opcode(0x59) 135 | GAS = _opcode(0x5a) 136 | 137 | LOG0 = _opcode(0xa0) 138 | LOG1 = _opcode(0xa1) 139 | LOG2 = _opcode(0xa2) 140 | LOG3 = _opcode(0xa3) 141 | LOG4 = _opcode(0xa4) 142 | 143 | CREATE = _opcode(0xf0) 144 | CALL = _opcode(0xf1) 145 | CALLCODE = _opcode(0xf2) 146 | RETURN = _opcode(0xf3) 147 | DELEGATECALL = _opcode(0xf4) 148 | STATICCALL = _opcode(0xfa) 149 | REVERT = _opcode(0xfd) 150 | INVALID = _opcode(0xfe) 151 | SELFDESTRUCT = _opcode(0xff) 152 | 153 | class Codegen(object): 154 | def __init__(self, code=None): 155 | self.code = bytearray() 156 | self._labels = dict() 157 | self._jumps = defaultdict(list) 158 | if code is not None: 159 | self.append(code) 160 | 161 | def createTxData(self): 162 | if len(self._jumps): 163 | raise RuntimeError("Pending labels: " + ','.join(self._jumps.keys())) 164 | 165 | return type(self)([ 166 | PUSH(len(self.code)), # length of code being deployed 167 | DUP(0), 168 | DUP(0), 169 | CODESIZE, # total length 170 | SUB, # codeOffset = (total_length - body_length) 171 | PUSH(0), # memOffset 172 | CODECOPY, 173 | PUSH(0), 174 | RETURN 175 | ]).code + self.code 176 | 177 | def append(self, *args): 178 | for arg in args: 179 | if isinstance(arg, (list, tuple)): 180 | # Allow x.append([opcode, opcode, ...]) 181 | arg = self.append(*arg) 182 | continue 183 | if isinstance(arg, PUSHLABEL): 184 | offset = None 185 | if arg.target is not None: 186 | if arg.target not in self._labels: 187 | self._jumps[arg.target].append(len(self.code)) 188 | offset = 0 # jump destination filled-in later 189 | else: 190 | offset = self._labels[arg.target] 191 | from binascii import hexlify 192 | self.code += arg.data(offset) 193 | elif isinstance(arg, LABEL): 194 | if arg.name in self._labels: 195 | raise RuntimeError("Cannot re-define label %r" % (arg.name,)) 196 | self._labels[arg.name] = len(self.code) 197 | if arg.name in self._jumps: 198 | for jump in self._jumps[arg.name]: 199 | self.code[jump+1:jump+4] = _encode_offset(len(self.code)) 200 | del self._jumps[arg.name] 201 | self.code += arg.data() 202 | elif isinstance(arg, _opcode): 203 | self.code += arg.data() 204 | else: 205 | raise RuntimeError("Unknown opcode %r" % (arg,)) 206 | -------------------------------------------------------------------------------- /sdk/ethsnarks/field.py: -------------------------------------------------------------------------------- 1 | # Code copied from https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_curve.py 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2015 Vitalik Buterin 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | # THE SOFTWARE. 24 | # 25 | 26 | import sys 27 | import bitstring 28 | from math import ceil, log2 29 | from os import urandom 30 | from collections import defaultdict 31 | from .numbertheory import square_root_mod_prime 32 | 33 | # python3 compatibility 34 | if sys.version_info.major > 2: 35 | int_types = (int,) 36 | long = int 37 | else: 38 | int_types = (int, long) # noqa: F821 39 | 40 | # Fq is the base field of Jubjub 41 | SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617 42 | # Fr is the scalar field of Jubjub 43 | FR_ORDER = 21888242871839275222246405745257275088614511777268538073601725287587578984328 44 | 45 | # A class for field elements in FQ. Wrap a number in this class, 46 | # and it becomes a field element. 47 | class FQ(object): 48 | _COUNTS = None 49 | 50 | @classmethod 51 | def _disable_counting(cls): 52 | cls._COUNTS = None 53 | 54 | @classmethod 55 | def _print_counts(cls): 56 | for k in sorted(cls._COUNTS.keys()): 57 | print(k, "=", cls._COUNTS[k]) 58 | print() 59 | 60 | @classmethod 61 | def _count(cls, what): 62 | if cls._COUNTS is not None: 63 | cls._COUNTS[what] += 1 64 | 65 | @classmethod 66 | def _reset_counts(cls): 67 | cls._COUNTS = defaultdict(int) 68 | 69 | def __init__(self, n, field_modulus=SNARK_SCALAR_FIELD): 70 | if isinstance(n, self.__class__): 71 | if n.m != field_modulus: 72 | raise ValueError("Field modulus mismatch") 73 | self.m = n.m 74 | self.n = n.n 75 | else: 76 | if not isinstance(n, int_types): 77 | raise ValueError("Invalid number type: " + str(type(n))) 78 | if not isinstance(field_modulus, int_types): 79 | raise ValueError("Invalid modulus type: " + str(type(field_modulus))) 80 | self.m = field_modulus 81 | self.n = n % self.m 82 | 83 | def __int__(self): 84 | return self.n 85 | 86 | def __hash__(self): 87 | return hash((self.n, self.m)) 88 | 89 | def _other_n(self, other): 90 | if isinstance(other, FQ): 91 | if other.m != self.m: 92 | raise RuntimeError("Other field element has different modulus") 93 | return other.n 94 | if not isinstance(other, int_types): 95 | raise RuntimeError("Not a valid value type: " + str(type(other).__name__)) 96 | return other 97 | 98 | def __add__(self, other): 99 | on = self._other_n(other) 100 | self._count('add') 101 | return FQ((self.n + on) % self.m, self.m) 102 | 103 | def __mul__(self, other): 104 | on = self._other_n(other) 105 | self._count('mul') 106 | return FQ((self.n * on) % self.m, self.m) 107 | 108 | def __rmul__(self, other): 109 | return self * other 110 | 111 | def __radd__(self, other): 112 | return self + other 113 | 114 | def __pow__(self, e): 115 | return self.exp(e) 116 | 117 | def __rsub__(self, other): 118 | on = self._other_n(other) 119 | self._count('sub') 120 | return FQ((on - self.n) % self.m, self.m) 121 | 122 | def __sub__(self, other): 123 | on = self._other_n(other) 124 | self._count('sub') 125 | return FQ((self.n - on) % self.m, self.m) 126 | 127 | def to_bytes(self, endian='big'): 128 | nbits = ceil(log2(self.m)) 129 | nbits += 8 - (nbits % 8) 130 | nbytes = nbits // 8 131 | return self.n.to_bytes(nbytes, endian) 132 | 133 | def bits(self): 134 | # TODO: endian 135 | nbits = ceil(log2(self.m)) 136 | bits = bin(self.n)[2:][::-1].ljust(nbits, '0') 137 | return bitstring.BitArray('0b' + bits) 138 | 139 | def inv(self): 140 | self._count('inv') 141 | return FQ(pow(self.n, self.m - 2, self.m), self.m) 142 | 143 | def sqrt(self): 144 | self._count('sqrt') 145 | return FQ(square_root_mod_prime(self.n, self.m), self.m) 146 | 147 | def exp(self, e): 148 | e = self._other_n(e) 149 | self._count('exp') 150 | return FQ(pow(self.n, e, self.m), self.m) 151 | 152 | def __div__(self, other): 153 | on = self._other_n(other) 154 | self._count('inv') 155 | return FQ((self.n * pow(on, self.m-2, self.m)) % self.m, self.m) 156 | 157 | def __floordiv__(self, other): 158 | return self.__div__(other) 159 | 160 | def __truediv__(self, other): 161 | return self.__div__(other) 162 | 163 | def __rdiv__(self, other): 164 | on = self._other_n(other) 165 | self._count('inv') 166 | self._count('mul') 167 | return FQ((pow(self.n, self.m-2, self.m) * on) % self.m, self.m) 168 | 169 | def __rtruediv__(self, other): 170 | return self.__rdiv__(other) 171 | 172 | def __eq__(self, other): 173 | if other == 0.: 174 | other = 0 175 | # TODO: verify modulus matches other? 176 | return self.n == self._other_n(other) 177 | 178 | def __ne__(self, other): 179 | return not self == other 180 | 181 | def __neg__(self): 182 | self._count('sub') 183 | return FQ(-self.n, self.m) 184 | 185 | def __repr__(self): 186 | return repr(self.n) 187 | 188 | @classmethod 189 | def random(cls, modulus=SNARK_SCALAR_FIELD): 190 | if isinstance(modulus, FQ): 191 | modulus = modulus.m 192 | nbytes = ceil(ceil(log2(modulus)) / 8) + 1 193 | rand_n = int.from_bytes(urandom(nbytes), 'little') 194 | return FQ(rand_n, modulus) 195 | 196 | @classmethod 197 | def one(self, modulus=SNARK_SCALAR_FIELD): 198 | if isinstance(modulus, FQ): 199 | modulus = modulus.m 200 | return FQ(1, modulus) 201 | 202 | @classmethod 203 | def zero(self, modulus=SNARK_SCALAR_FIELD): 204 | if isinstance(modulus, FQ): 205 | modulus = modulus.m 206 | return FQ(0, modulus) 207 | 208 | class FR(FQ): 209 | def __init__(self, n, field_modulus=FR_ORDER): 210 | FQ.__init__(self, n, field_modulus) 211 | -------------------------------------------------------------------------------- /sdk/ethsnarks/jubjub.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements the extended twisted edwards and extended affine coordinates 3 | described in the paper "Twisted Edwards Curves Revisited": 4 | 5 | - https://iacr.org/archive/asiacrypt2008/53500329/53500329.pdf 6 | Huseyin Hisil, Kenneth Koon-Ho Wong, Gary Carter, and Ed Dawson 7 | 8 | Information Security Institute, 9 | Queensland University of Technology, QLD, 4000, Australia 10 | {h.hisil, kk.wong, g.carter, e.dawson}@qut.edu.au 11 | 12 | By using the extended coordinate system we can avoid expensive modular exponentiation 13 | calls, for example - a scalar multiplication call (or multiple...) may perform only 14 | one 3d->2d projection at the point where affine coordinates are necessary, and every 15 | intermediate uses a much faster form. 16 | 17 | # XXX: none of these functions are constant time, they should not be used interactively! 18 | """ 19 | 20 | from os import urandom 21 | from hashlib import sha256 22 | from collections import namedtuple 23 | 24 | from .field import FQ, SNARK_SCALAR_FIELD 25 | from .numbertheory import SquareRootError 26 | 27 | 28 | JUBJUB_Q = SNARK_SCALAR_FIELD 29 | JUBJUB_E = 21888242871839275222246405745257275088614511777268538073601725287587578984328 # #E is the order of the curve E 30 | JUBJUB_C = 8 # Cofactor 31 | JUBJUB_L = JUBJUB_E//JUBJUB_C # L*B = 0, and (2^C)*L == #E 32 | JUBJUB_A = 168700 # Coefficient A 33 | JUBJUB_D = 168696 # Coefficient D 34 | 35 | 36 | # Verify JUBJUB_A is a non-zero square 37 | try: 38 | FQ(JUBJUB_A).sqrt() 39 | except SquareRootError: 40 | raise RuntimeError("JUBJUB_A is required to be a square") 41 | 42 | # Verify JUBJUB_D is non-square 43 | try: 44 | FQ(JUBJUB_D).sqrt() 45 | raise RuntimeError("JUBJUB_D is required to be non-square") 46 | except SquareRootError: 47 | pass 48 | 49 | 50 | """ 51 | From "Twisted Edwards Curves", 2008-BBJLP 52 | Theorem 3.2 53 | """ 54 | MONT_A = 168698 # int(2*(JUBJUB_A+JUBJUB_D)/(JUBJUB_A-JUBJUB_D)) 55 | MONT_B = 1 # int(4/(JUBJUB_A-JUBJUB_D)) 56 | MONT_A24 = int((MONT_A+2)/4) 57 | assert MONT_A24*4 == MONT_A+2 58 | 59 | 60 | """ 61 | 2017-BL - "Montgomery curves and the Montgomery ladder" 62 | - https://eprint.iacr.org/2017/293.pdf 63 | 4.3.5, The curve parameters satisfy: 64 | """ 65 | assert JUBJUB_A == (MONT_A+2)/MONT_B 66 | assert JUBJUB_D == (MONT_A-2)/MONT_B 67 | 68 | 69 | def is_negative(v): 70 | assert isinstance(v, FQ) 71 | return v.n > (-v).n 72 | 73 | 74 | class AbstractCurveOps(object): 75 | def __neg__(self): 76 | return self.neg() 77 | 78 | def __add__(self, other): 79 | return self.add(other) 80 | 81 | def __sub__(self, other): 82 | return self.add(other.neg()) 83 | 84 | def __mul__(self, n): 85 | return self.mult(n) 86 | 87 | def double(self): 88 | return self.add(self) 89 | 90 | def rescale(self): 91 | return self 92 | 93 | def compress(self): 94 | return self.as_point.compress() 95 | 96 | @classmethod 97 | def all_loworder_points(cls): 98 | """ 99 | All low-order points 100 | """ 101 | return [ 102 | Point(FQ(0), FQ(1)), 103 | Point(FQ(0), FQ(21888242871839275222246405745257275088548364400416034343698204186575808495616)), 104 | Point(FQ(2957874849018779266517920829765869116077630550401372566248359756137677864698), FQ(0)), 105 | Point(FQ(4342719913949491028786768530115087822524712248835451589697801404893164183326), FQ(4826523245007015323400664741523384119579596407052839571721035538011798951543)), 106 | Point(FQ(4342719913949491028786768530115087822524712248835451589697801404893164183326), FQ(17061719626832259898845741003733890968968767993363194771977168648564009544074)), 107 | Point(FQ(17545522957889784193459637215142187266023652151580582754000402781682644312291), FQ(4826523245007015323400664741523384119579596407052839571721035538011798951543)), 108 | Point(FQ(17545522957889784193459637215142187266023652151580582754000402781682644312291), FQ(17061719626832259898845741003733890968968767993363194771977168648564009544074)), 109 | Point(FQ(18930368022820495955728484915491405972470733850014661777449844430438130630919), FQ(0)) 110 | ] 111 | 112 | @classmethod 113 | def decompress(cls, point): 114 | return Point.decompress(point).as_proj() 115 | 116 | def is_negative(self): 117 | """ 118 | The point is negative if the X coordinate is lower than its modulo negative 119 | """ 120 | return is_negative(self.as_point().x) 121 | 122 | def sign(self): 123 | return 1 if self.is_negative() else 0 124 | 125 | def mult_wnaf(self, scalar, window=5): 126 | return mult_naf_lut(self, scalar, window) 127 | 128 | def mult(self, scalar): 129 | if isinstance(scalar, FQ): 130 | if scalar.m not in [SNARK_SCALAR_FIELD, JUBJUB_E, JUBJUB_L]: 131 | raise ValueError("Invalid field modulus") 132 | scalar = scalar.n 133 | p = self 134 | a = self.infinity() 135 | i = 0 136 | while scalar != 0: 137 | if (scalar & 1) != 0: 138 | a = a.add(p) 139 | p = p.double() 140 | scalar = scalar // 2 141 | i += 1 142 | return a 143 | 144 | 145 | class Point(AbstractCurveOps, namedtuple('_Point', ('x', 'y'))): 146 | def __str__(self): 147 | return ' '.join([str(_) for _ in self]) 148 | 149 | @classmethod 150 | def from_y(cls, y, sign=None): 151 | """ 152 | x^2 = (y^2 - 1) / (d * y^2 - a) 153 | """ 154 | assert isinstance(y, FQ) 155 | assert y.m == JUBJUB_Q 156 | ysq = y * y 157 | lhs = (ysq - 1) 158 | rhs = (JUBJUB_D * ysq - JUBJUB_A) 159 | xsq = lhs / rhs 160 | x = xsq.sqrt() 161 | if sign is not None: 162 | # Used for compress & decompress 163 | if is_negative(x) ^ (sign != 0): 164 | x = -x 165 | else: 166 | if is_negative(x): 167 | x = -x 168 | return cls(x, y) 169 | 170 | @classmethod 171 | def from_x(cls, x): 172 | """ 173 | y^2 = ((a * x^2) / (d * x^2 - 1)) - (1 / (d * x^2 - 1)) 174 | 175 | For every x coordinate, there are two possible points: (x, y) and (x, -y) 176 | """ 177 | assert isinstance(x, FQ) 178 | assert x.m == JUBJUB_Q 179 | xsq = x * x 180 | ax2 = JUBJUB_A * xsq 181 | dxsqm1 = (JUBJUB_D * xsq - 1).inv() 182 | ysq = dxsqm1 * (ax2 - 1) 183 | y = ysq.sqrt() 184 | return cls(x, y) 185 | 186 | @classmethod 187 | def random(cls): 188 | return cls.from_hash(urandom(32)) 189 | 190 | @classmethod 191 | def from_hash(cls, entropy): 192 | """ 193 | HashToPoint (or Point.from_hash) 194 | 195 | Hashes the input entropy and interprets the result as the Y coordinate 196 | then recovers the X coordinate, if no valid point can be recovered 197 | Y is incremented until a matching X coordinate is found. 198 | 199 | The point is guaranteed to be prime order and not the identity. 200 | 201 | From: https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/?include_text=1 202 | 203 | Page 6: 204 | 205 | o HashToBase(x, i). This method is parametrized by p and H, where p 206 | is the prime order of the base field Fp, and H is a cryptographic 207 | hash function which outputs at least floor(log2(p)) + 2 bits. The 208 | function first hashes x, converts the result to an integer, and 209 | reduces modulo p to give an element of Fp. 210 | """ 211 | assert isinstance(entropy, bytes) 212 | entropy = sha256(entropy).digest() 213 | entropy_as_int = int.from_bytes(entropy, 'big') 214 | y = FQ(entropy_as_int) 215 | while True: 216 | try: 217 | p = cls.from_y(y) 218 | except SquareRootError: 219 | y += 1 220 | continue 221 | 222 | # Multiply point by cofactor, ensures it's on the prime-order subgroup 223 | p = p * JUBJUB_C 224 | 225 | # Verify point is on prime-ordered sub-group 226 | if (p * JUBJUB_L) != Point.infinity(): 227 | raise RuntimeError("Point not on prime-ordered subgroup") 228 | 229 | return p 230 | 231 | def __eq__(self, other): 232 | return self.x == other.x and self.y == other.y 233 | 234 | def __hash__(self): 235 | return hash((self.x, self.y)) 236 | 237 | def compress(self): 238 | x = self.x 239 | y = self.y.n 240 | return int.to_bytes(y | (is_negative(x) << 255), 32, "little") 241 | 242 | @classmethod 243 | def decompress(cls, point): 244 | """ 245 | From: https://ed25519.cr.yp.to/eddsa-20150704.pdf 246 | 247 | The encoding of F_q is used to define "negative" elements of F_q: 248 | specifically, x is negative if the (b-1)-bit encoding of x is 249 | lexiographically larger than the (b-1)-bit encoding of -x. In particular, 250 | if q is prime and the (b-1)-bit encoding of F_q is the little-endian 251 | encoding of {0, 1, ..., q-1}, then {1,3,5,...,q-2} are the negative element of F_q. 252 | 253 | This encoding is also used to define a b-bit encoding of each element `(x,y) ∈ E` 254 | as a b-bit string (x,y), namely the (b-1)-bit encoding of y followed by the sign bit. 255 | the sign bit is 1 if and only if x is negative. 256 | 257 | A parser recovers `(x,y)` from a b-bit string, while also verifying that `(x,y) ∈ E`, 258 | as follows: parse the first b-1 bits as y, compute `xx = (y^2 - 1) / (dy^2 - a)`; 259 | compute `x = [+ or -] sqrt(xx)` where the `[+ or -]` is chosen so that the sign of 260 | `x` matches the `b`th bit of the string. if `xx` is not a square then parsing fails. 261 | """ 262 | if len(point) != 32: 263 | raise ValueError("Invalid input length for decompression") 264 | y = int.from_bytes(point, "little") 265 | sign = y >> 255 266 | y &= (1 << 255) - 1 267 | return cls.from_y(FQ(y), sign) 268 | 269 | def as_mont(self): 270 | return MontPoint.from_edwards(self) 271 | 272 | def as_proj(self): 273 | return ProjPoint(self.x, self.y, FQ(1)) 274 | 275 | def as_etec(self): 276 | return EtecPoint(self.x, self.y, self.x*self.y, FQ(1)) 277 | 278 | def as_point(self): 279 | return self 280 | 281 | def neg(self): 282 | """ 283 | Twisted Edwards Curves, BBJLP-2008, section 2 pg 2 284 | """ 285 | return Point(-self.x, self.y) 286 | 287 | @classmethod 288 | def generator(cls): 289 | x = 16540640123574156134436876038791482806971768689494387082833631921987005038935 290 | y = 20819045374670962167435360035096875258406992893633759881276124905556507972311 291 | return Point(FQ(x), FQ(y)) 292 | 293 | 294 | def valid(self): 295 | """ 296 | Satisfies the relationship 297 | 298 | ax^2 + y^2 = 1 + d x^2 y^2 299 | """ 300 | xsq = self.x*self.x 301 | ysq = self.y*self.y 302 | return (JUBJUB_A * xsq) + ysq == (1 + JUBJUB_D * xsq * ysq) 303 | 304 | def add(self, other): 305 | assert isinstance(other, Point) 306 | if self.x == 0 and self.y == 0: 307 | return other 308 | (u1, v1) = (self.x, self.y) 309 | (u2, v2) = (other.x, other.y) 310 | u3 = (u1*v2 + v1*u2) / (FQ.one() + JUBJUB_D*u1*u2*v1*v2) 311 | v3 = (v1*v2 - JUBJUB_A*u1*u2) / (FQ.one() - JUBJUB_D*u1*u2*v1*v2) 312 | return Point(u3, v3) 313 | 314 | @staticmethod 315 | def infinity(): 316 | return Point(FQ(0), FQ(1)) 317 | 318 | 319 | class ProjPoint(AbstractCurveOps, namedtuple('_ProjPoint', ('x', 'y', 'z'))): 320 | def rescale(self): 321 | return ProjPoint(self.x / self.z, self.y / self.z, FQ(1)) 322 | 323 | def as_proj(self): 324 | return self 325 | 326 | def __eq__(self, other): 327 | return self.x == other.x and self.y == other.y and self.z == other.z 328 | 329 | def __hash__(self): 330 | return hash((self.x, self.y, self.z)) 331 | 332 | def as_etec(self): 333 | """ 334 | (X, Y, Z) -> (X, Y, X*Y, Z) 335 | """ 336 | return EtecPoint(self.x, self.y, self.x*self.y, self.z) 337 | 338 | def as_mont(self): 339 | return self.as_point().as_mont() 340 | 341 | def as_point(self): 342 | assert self.z != 0 343 | inv_z = self.z.inv() 344 | return Point(self.x*inv_z, self.y*inv_z) 345 | 346 | def valid(self): 347 | return self.as_point().valid() 348 | 349 | @staticmethod 350 | def infinity(): 351 | return ProjPoint(FQ(0), FQ(1), FQ(1)) 352 | 353 | def neg(self): 354 | """ 355 | -(X : Y : Z) = (-X : Y : Z) 356 | """ 357 | return ProjPoint(-self.x, self.y, self.z) 358 | 359 | def add(self, other): 360 | """ 361 | add-2008-bbjlp 362 | https:/eprint.iacr.org/2008/013 Section 6 363 | Strongly unified 364 | """ 365 | assert isinstance(other, ProjPoint) 366 | if self == self.infinity(): 367 | return other 368 | a = self.z * other.z 369 | b = a * a 370 | c = self.x * other.x 371 | d = self.y * other.y 372 | t0 = c * d 373 | e = JUBJUB_D * t0 374 | f = b - e 375 | g = b + e 376 | t1 = self.x + self.y 377 | t2 = other.x + other.y 378 | t3 = t1 * t2 379 | t4 = t3 - c 380 | t5 = t4 - d 381 | t6 = f * t5 382 | x3 = a * t6 383 | t7 = JUBJUB_A * c 384 | t8 = d - t7 385 | t9 = g * t8 386 | y3 = a * t9 387 | z3 = f * g 388 | return ProjPoint(x3, y3, z3) 389 | 390 | def double(self): 391 | """ 392 | dbl-2008-bbjlp https://eprint.iacr.org/2008/013 393 | 394 | From "Twisted Edwards Curves" - BBJLP 395 | 396 | # Doubling in Projective Twisted Coordinates 397 | > The following formulas compute (X3 : Y3 : Z3) = 2(X1 : Y1 : Z1) 398 | > in 3M + 4S + 1D + 7add, where the 1D is a multiplication by `a`. 399 | """ 400 | if self == self.infinity(): 401 | return self.infinity() 402 | t0 = self.x + self.y 403 | b = t0 * t0 404 | c = self.x * self.x 405 | d = self.y * self.y 406 | e = JUBJUB_A * c 407 | f = e + d 408 | h = self.z * self.z 409 | t1 = 2 * h 410 | j = f - t1 411 | t2 = b - c 412 | t3 = t2 - d 413 | x3 = t3 * j 414 | t4 = e - d 415 | y3 = f * t4 416 | z3 = f * j 417 | return ProjPoint(x3, y3, z3) 418 | 419 | 420 | class MontPoint(AbstractCurveOps, namedtuple('_MontPoint', ('u', 'v'))): 421 | @classmethod 422 | def from_edwards(cls, e): 423 | """ 424 | The map from a twisted Edwards curve is defined as 425 | 426 | (x, y) -> (u, v) where 427 | u = (1 + y) / (1 - y) 428 | v = u / x 429 | 430 | This mapping is not defined for y = 1 and for x = 0. 431 | 432 | We have that y != 1 above. If x = 0, the only 433 | solutions for y are 1 (contradiction) or -1. 434 | 435 | See: https://github.com/zcash/librustzcash/blob/master/sapling-crypto/src/jubjub/montgomery.rs#L121 436 | """ 437 | e = e.as_point() 438 | if e.y == FQ.one(): 439 | # The only solution for y = 1 is x = 0. (0, 1) is 440 | # the neutral element, so we map this to the point at infinity. 441 | return MontPoint(FQ.zero(), FQ.one()) 442 | if e.x == FQ.zero(): 443 | return MontPoint(FQ.zero(), FQ.zero()) 444 | u = (FQ.one() + e.y) / (FQ.one() - e.y) 445 | v = u / e.x 446 | return cls(u, v) 447 | 448 | def as_point(self): 449 | """ 450 | See: https://eprint.iacr.org/2008/013.pdf 451 | - "Twisted Edwards Curves" (BBJLP'08) 452 | - Theorem 3.2 pg 4 453 | 454 | with inverse 455 | (u, v) → (x, y) = (u/v, (u − 1)/(u + 1)). 456 | """ 457 | x = self.u / self.v 458 | y = (self.u - 1) / (self.u + 1) 459 | return Point(x, y) 460 | 461 | def as_etec(self): 462 | return self.as_point().as_etec() 463 | 464 | def as_proj(self): 465 | return self.as_point().as_proj() 466 | 467 | def valid(self): 468 | """ 469 | See: https://eprint.iacr.org/2008/013.pdf 470 | - "Twisted Edwards Curves" (BBJLP'08) 471 | 472 | Definition 3.1 (Montgomery curve). Fix a field k with char(k) 6= 2. Fix 473 | A ∈ k \ {−2, 2} and B ∈ k \ {0}. 474 | 475 | The Montgomery curve with coefficients A and B is the curve 476 | 477 | E_{M,A,B} : B * (v^2) = u^3 + A*(u^2) + u 478 | """ 479 | lhs = MONT_B * (self.v ** 2) 480 | rhs = (self.u ** 3) + MONT_A * (self.u ** 2) + self.u 481 | return lhs == rhs 482 | 483 | def as_mont(self): 484 | return self 485 | 486 | @classmethod 487 | def infinity(cls): 488 | return cls(FQ(0), FQ(1)) 489 | 490 | def neg(self): 491 | return type(self)(self.u, -self.v) 492 | 493 | def __eq__(self, other): 494 | return self.u == other.u and self.v == other.v 495 | 496 | def __hash__(self): 497 | return hash((self.u, self.v)) 498 | 499 | def double(self): 500 | # https://github.com/zcash/librustzcash/blob/master/sapling-crypto/src/jubjub/montgomery.rs#L224 501 | # See §4.3.2 The group law for Weierstrass curves 502 | # - Montgomery curves and the Montgomery Ladder 503 | # - Daniel J. Bernstein and Tanja Lange 504 | # @ https://cr.yp.to/papers/montladder-20170330.pdf 505 | if self.v == FQ.zero(): 506 | return self.infinity() 507 | 508 | usq = self.u * self.u 509 | delta = (1 + (2*(MONT_A * self.u)) + usq + (usq*2)) / (2*self.v) 510 | x3 = (delta*delta) - MONT_A - (2*self.u) 511 | y3 = -((x3 - self.u) * delta + self.v) 512 | return type(self)(x3, y3) 513 | 514 | def add(self, other): 515 | # https://github.com/zcash/librustzcash/blob/master/sapling-crypto/src/jubjub/montgomery.rs#L288 516 | other = other.as_mont() 517 | infinity = self.infinity() 518 | if other == infinity: 519 | return self 520 | elif self == infinity: 521 | return other 522 | 523 | if self.u == other.u: 524 | if self.v == other.v: 525 | return self.double() 526 | return infinity 527 | 528 | delta = (other.v - self.v) / (other.u - self.u) 529 | x3 = (delta*delta) - MONT_A - self.u - other.u 530 | y3 = -((x3 - self.u) * delta + self.v) 531 | return type(self)(x3, y3) 532 | 533 | 534 | class EtecPoint(AbstractCurveOps, namedtuple('_EtecPoint', ('x', 'y', 't', 'z'))): 535 | def as_etec(self): 536 | return self 537 | 538 | def __eq__(self, other): 539 | return self.x == other.x and self.y == other.y and self.t == other.t and self.z == other.z 540 | 541 | def __hash__(self): 542 | return hash((self.x, self.y, self.t, self.z)) 543 | 544 | def as_mont(self): 545 | return self.as_point().as_mont() 546 | 547 | def as_point(self): 548 | """ 549 | Ignoring the T value, project from 3d X,Y,Z to 2d X,Y coordinates 550 | 551 | (X : Y : T : Z) -> (X/Z, Y/Z) 552 | """ 553 | inv_z = self.z.inv() 554 | return Point(self.x*inv_z, self.y*inv_z) 555 | 556 | def as_proj(self): 557 | """ 558 | The T value is dropped when converting from extended 559 | twisted edwards to projective edwards coordinates. 560 | 561 | (X : Y : T : Z) -> (X, Y, Z) 562 | """ 563 | return ProjPoint(self.x, self.y, self.z) 564 | 565 | @staticmethod 566 | def infinity(): 567 | return EtecPoint(FQ(0), FQ(1), FQ(0), FQ(1)) 568 | 569 | def neg(self): 570 | """ 571 | Twisted Edwards Curves Revisited - HWCD, pg 5, section 3 572 | 573 | -(X : Y : T : Z) = (-X : Y : -T : Z) 574 | """ 575 | return EtecPoint(-self.x, self.y, -self.t, self.z) 576 | 577 | def valid(self): 578 | return self.as_point().valid() 579 | 580 | def double(self): 581 | """ 582 | dbl-2008-hwcd 583 | """ 584 | if self == self.infinity(): 585 | return self.infinity() 586 | a = self.x * self.x 587 | b = self.y * self.y 588 | t0 = self.z * self.z 589 | c = t0 * 2 590 | d = JUBJUB_A * a 591 | t1 = self.x + self.y 592 | t2 = t1 * t1 593 | t3 = t2 - a 594 | e = t3 - b 595 | g = d + b 596 | f = g - c 597 | h = d - b 598 | return EtecPoint(e*f, g*h, e*h, f*g) 599 | 600 | def add(self, other): 601 | """ 602 | 3.1 Unified addition in ε^e 603 | """ 604 | assert isinstance(other, EtecPoint) 605 | if self == self.infinity(): 606 | return other 607 | 608 | assert self.z != 0 609 | assert other.z != 0 610 | 611 | x1x2 = self.x * other.x 612 | y1y2 = self.y * other.y 613 | dt1t2 = (JUBJUB_D * self.t) * other.t 614 | z1z2 = self.z * other.z 615 | e = ((self.x + self.y) * (other.x + other.y)) - x1x2 - y1y2 616 | f = z1z2 - dt1t2 617 | g = z1z2 + dt1t2 618 | h = y1y2 - (JUBJUB_A * x1x2) 619 | 620 | return EtecPoint(e*f, g*h, e*h, f*g) 621 | 622 | 623 | def wNAF(k, width=2): 624 | # windowed Non-Adjacent-Form 625 | # https://bristolcrypto.blogspot.com/2015/04/52-things-number-26-describe-naf-scalar.html 626 | # https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication#w-ary_non-adjacent_form_(wNAF)_method 627 | k = int(k) 628 | a = 2**width 629 | b = 2**(width-1) 630 | output = [] 631 | while k > 0: 632 | if (k % 2) == 1: 633 | c = k % a 634 | if c > b: 635 | k_i = c - a 636 | else: 637 | k_i = c 638 | k = k - k_i 639 | else: 640 | k_i = 0 641 | output.append(k_i) 642 | k = k // 2 643 | return output[::-1] 644 | 645 | 646 | def naf_window(point, nbits): 647 | """ 648 | Return an n-bit window of points for use with w-NAF multiplication 649 | """ 650 | a = (1< 2 and n % 2 == 0: 658 | continue 659 | else: 660 | p_n = res[n-2] + res[2] 661 | res[n] = p_n 662 | res[-n] = -p_n 663 | return res 664 | 665 | 666 | def mult_naf(point, scalar): 667 | # Multiplication using NAF 668 | a = point.infinity() 669 | for k_i in wNAF(scalar): 670 | a = a.double() 671 | if k_i == 1: 672 | a = a.add(point) 673 | elif k_i == -1: 674 | a = a.add(point.neg()) 675 | return a 676 | 677 | 678 | def mult_naf_lut(point, scalar, width=2): 679 | # Multipication using Windowed NAF, with an arbitrary sized window 680 | a = point.infinity() 681 | w = naf_window(point, width) 682 | for k_i in wNAF(scalar, width): 683 | a = a.double() 684 | p = w[k_i] 685 | if p is not None: 686 | a = a.add(p) 687 | return a 688 | -------------------------------------------------------------------------------- /sdk/ethsnarks/merkletree.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018-2019 HarryR 2 | # License: LGPL-3.0+ 3 | 4 | import hashlib 5 | import math 6 | from collections import namedtuple 7 | 8 | from .poseidon import poseidon, DefaultParams as poseidon_DefaultParams 9 | from .mimc import mimc_hash 10 | from .field import FQ, SNARK_SCALAR_FIELD 11 | 12 | 13 | class MerkleProof(namedtuple('_MerkleProof', ('leaf', 'address', 'path', 'hasher', 'width'))): 14 | def verify(self, root): 15 | item = self.leaf 16 | for depth, (index, proof) in enumerate(zip(self.address, self.path)): 17 | hasher_args = proof[::] if isinstance(proof, list) else [proof] 18 | hasher_args.insert(index, item) 19 | item = self.hasher.hash_node(depth, *hasher_args) 20 | return root == item 21 | 22 | 23 | class Abstract_MerkleHasher(object): 24 | def unique(self, depth, index): 25 | """ 26 | Derive a unique hash for a leaf which doesn't exist in the tree 27 | This allows for an incremental tree to be constructed, where the 28 | remaining nodes which don't exist yet are 'filled-in' with these placeholders 29 | """ 30 | assert depth < self._tree_depth 31 | item = int(depth).to_bytes(2, 'big') + int(index).to_bytes(30, 'big') 32 | hasher = hashlib.sha256() 33 | hasher.update(item) 34 | return int.from_bytes(hasher.digest(), 'big') % SNARK_SCALAR_FIELD 35 | 36 | def _make_IVs(self): 37 | out = [] 38 | hasher = hashlib.sha256() 39 | for i in range(self._tree_depth): 40 | item = int(i).to_bytes(2, 'little') 41 | hasher.update(b'MerkleTree-' + item) 42 | digest = int.from_bytes(hasher.digest(), 'big') % SNARK_SCALAR_FIELD 43 | out.append(digest) 44 | return out 45 | 46 | def valid(self, item): 47 | return isinstance(item, int) and item > 0 and item < SNARK_SCALAR_FIELD 48 | 49 | 50 | # TODO: move to ethsnarks.mimc ? 51 | class MerkleHasher_MiMC(Abstract_MerkleHasher): 52 | def __init__(self, tree_depth, node_width=2): 53 | if node_width != 2: 54 | raise ValueError("Invalid node width %r, must be 2" % (node_width,)) 55 | self._tree_depth = tree_depth 56 | self._IVs = self._make_IVs() 57 | 58 | def hash_node(self, depth, *args): 59 | return mimc_hash(args, self._IVs[depth]) 60 | 61 | 62 | # TODO: move to ethsnarks.poseidon? 63 | class MerkleHasher_Poseidon(Abstract_MerkleHasher): 64 | def __init__(self, params, depth, node_width=2): 65 | assert node_width > 0 66 | if params is None: 67 | params = poseidon_DefaultParams 68 | if node_width >= (params.t - 1) or node_width <= 0: 69 | raise ValueError("Node width must be in range: 0 < width < (t-1)") 70 | self._params = params 71 | self._tree_depth = depth 72 | 73 | @classmethod 74 | def factory(cls, params=None): 75 | return lambda *args, **kwa: cls(params, *args, **kwa) 76 | 77 | def hash_node(self, depth, *args): 78 | return poseidon(args, params=self._params) 79 | 80 | 81 | DEFAULT_HASHER = MerkleHasher_MiMC 82 | 83 | 84 | class MerkleTree(object): 85 | """ 86 | With a tree of depth 2 and width 4, contains 16 items: 87 | 88 | offsets: 0 1 2 3 4 5 6 7 8 9 . . . . . . 89 | level 0: [A B C D] [E F G H] [I J K L] [M N O P] 90 | level 1: [Q R S T] 91 | level 2: [U] 92 | 93 | Our item is `G`, which is at position `[1][2]` 94 | The tree is equivalent to: 95 | 96 | level1: [Q=H(A,B,C,D) R=H(E,F G H) S=H(I,J,K,L) T=H(M,N,O,P)] 97 | level2: [U=H(Q,R,S,T)] 98 | 99 | The proof for our item `G` will be: 100 | 101 | [(2, [E F H]), (1, [Q S T])] 102 | 103 | Each element of the proof supplies the index that the previous output will be inserted 104 | into the list of other elements in the hash to re-construct the root 105 | """ 106 | def __init__(self, n_items, width=2, hasher=None): 107 | assert n_items >= width 108 | assert (n_items % width) == 0 109 | if hasher is None: 110 | hasher = DEFAULT_HASHER 111 | self._width = width 112 | self._tree_depth = int(math.log(n_items, width)) 113 | self._hasher = hasher(self._tree_depth, width) 114 | self._n_items = n_items 115 | self._cur = 0 116 | self._leaves = [list() for _ in range(0, self._tree_depth + 1)] 117 | 118 | def __len__(self): 119 | return self._cur 120 | 121 | def update(self, index, leaf): 122 | if isinstance(leaf, FQ): 123 | leaf = leaf.n 124 | if not isinstance(leaf, int): 125 | raise TypeError("Invalid leaf") 126 | assert leaf >= 0 and leaf < SNARK_SCALAR_FIELD 127 | if (len(self._leaves[0]) - 1) < index: 128 | raise KeyError("Out of bounds") 129 | self._leaves[0][index] = leaf 130 | self._updateTree(index) 131 | 132 | def append(self, leaf): 133 | if self._cur >= (self._n_items): 134 | raise RuntimeError("Tree Full") 135 | if isinstance(leaf, FQ): 136 | leaf = leaf.n 137 | assert leaf >= 0 and leaf < SNARK_SCALAR_FIELD 138 | self._leaves[0].append(leaf) 139 | self._updateTree() 140 | self._cur += 1 141 | return self._cur - 1 142 | 143 | def __getitem__(self, key): 144 | if not isinstance(key, int): 145 | raise TypeError("Invalid key") 146 | if key < 0 or key >= self._cur: 147 | raise KeyError("Out of bounds") 148 | return self._leaves[0][key] 149 | 150 | def __setitem__(self, key, value): 151 | self.update(key, value) 152 | 153 | def __contains__(self, key): 154 | return key in self._leaves[0] 155 | 156 | def index(self, leaf): 157 | return self._leaves[0].index(leaf) 158 | 159 | def _make_node(self, depth, index): 160 | node_start = index - (index % self._width) 161 | return [self.leaf(depth, _) for _ in range(node_start, node_start + self._width)] 162 | 163 | def proof(self, index): 164 | leaf = self[index] 165 | if index >= self._cur: 166 | raise RuntimeError("Proof for invalid item!") 167 | address_bits = list() 168 | merkle_proof = list() 169 | for depth in range(self._tree_depth): 170 | proof_items = self._make_node(depth, index) 171 | proof_items.remove(proof_items[index % self._width]) 172 | if len(proof_items) == 1: 173 | proof_items = proof_items[0] 174 | address_bits.append( index % self._width ) 175 | merkle_proof.append( proof_items ) 176 | index = index // self._width 177 | return MerkleProof(leaf, address_bits, merkle_proof, self._hasher, self._width) 178 | 179 | def _updateTree(self, cur_index=None): 180 | cur_index = self._cur if cur_index is None else cur_index 181 | for depth in range(self._tree_depth): 182 | next_index = cur_index // self._width 183 | node_items = self._make_node(depth, cur_index) 184 | node = self._hasher.hash_node(depth, *node_items) 185 | if len(self._leaves[depth+1]) == next_index: 186 | self._leaves[depth+1].append(node) 187 | else: 188 | self._leaves[depth+1][next_index] = node 189 | cur_index = next_index 190 | 191 | def leaf(self, depth, offset): 192 | if offset >= len(self._leaves[depth]): 193 | return self._hasher.unique(depth, offset) 194 | return self._leaves[depth][offset] 195 | 196 | @property 197 | def root(self): 198 | if self._cur == 0: 199 | return None 200 | return self._leaves[self._tree_depth][0] 201 | -------------------------------------------------------------------------------- /sdk/ethsnarks/mimc/__init__.py: -------------------------------------------------------------------------------- 1 | from .permutation import mimc, mimc_hash, mimc_hash_md 2 | -------------------------------------------------------------------------------- /sdk/ethsnarks/mimc/contract.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Jordi Baylina 2 | # Copyright (c) 2019 Harry Roberts 3 | # License: LGPL-3.0+ 4 | # Based on: https://github.com/iden3/circomlib/blob/master/src/mimc_gencontract.js 5 | 6 | import sys 7 | import json 8 | from binascii import hexlify 9 | 10 | from ..sha3 import keccak_256 11 | from ..evmasm import * 12 | from ..field import SNARK_SCALAR_FIELD 13 | 14 | from .permutation import mimc_constants 15 | 16 | 17 | def _mimc_opcodes_round(exponent): 18 | # x = input 19 | # k = key 20 | # q = field modulus 21 | # stack upon entry: x q q k q 22 | # stack upon exit: r k q 23 | if exponent == 7: 24 | return [ 25 | DUP(3), # k x q q k q 26 | ADDMOD, # t=x+k q k q 27 | DUP(1), # q t q k q 28 | DUP(0), # q q t q k q 29 | DUP(2), # t q q t q k q 30 | DUP(0), # t t q q t q k q 31 | MULMOD(), # a=t^2 q t q k q 32 | DUP(1), # q a q t q k q 33 | DUP(1), # a q a q t q k q 34 | DUP(0), # a a q a q t q k q 35 | MULMOD, # b=t^4 a q t q k q 36 | MULMOD, # c=t^6 t q k q 37 | MULMOD # r=t^7 k q 38 | ] 39 | elif exponent == 5: 40 | return [ 41 | DUP(3), # k x q q k q 42 | ADDMOD, # t=x+k q k q 43 | DUP(1), # q t q k q 44 | DUP(0), # q q t q k q 45 | DUP(2), # t q q t q k q 46 | DUP(0), # t t q q t q k q 47 | MULMOD(), # a=t^2 q t q k q 48 | DUP(0), # a a q t q k q 49 | MULMOD, # b=t^4 t q k q 50 | MULMOD # r=t^5 k q 51 | ] 52 | 53 | 54 | def mimc_contract_opcodes(exponent): 55 | assert exponent in (5, 7) 56 | tag = keccak_256(f"MiMCpe{exponent}(uint256,uint256)".encode('ascii')).hexdigest() 57 | 58 | # Ensuring that `exponent ** n_rounds` > SNARK_SCALAR_FIELD 59 | n_rounds = 110 if exponent == 5 else 91 60 | constants = mimc_constants(R=n_rounds) 61 | 62 | yield [PUSH(0x44), # callDataLength 63 | PUSH(0), # callDataOffset 64 | PUSH(0), # memoryOffset 65 | CALLDATACOPY, 66 | PUSH(1<<224), 67 | PUSH(0), 68 | MLOAD, 69 | DIV, 70 | PUSH(int(tag[:8], 16)), # function selector 71 | EQ, 72 | JMPI('start'), 73 | INVALID] 74 | 75 | yield [LABEL('start'), 76 | PUSH(SNARK_SCALAR_FIELD), # q 77 | PUSH(0x24), 78 | MLOAD] # k q 79 | 80 | yield [ 81 | PUSH(0x04), # 0x04 k q 82 | MLOAD # x k q 83 | ] 84 | 85 | for c_i in constants: 86 | yield [ 87 | DUP(2), # q r k q 88 | DUP(0), # q q r k q 89 | DUP(0), # q q q r k q 90 | SWAP(3), # r q q q k q 91 | PUSH(c_i), # c r q q q k q 92 | ADDMOD, # c+r q q k q 93 | ] 94 | yield _mimc_opcodes_round(exponent) 95 | 96 | # add k to result, then return 97 | yield [ 98 | ADDMOD, # r+k 99 | PUSH(0), # r+k 0 100 | MSTORE, # 101 | PUSH(0x20), # 0x20 102 | PUSH(0), # 0 0x20 103 | RETURN 104 | ] 105 | 106 | 107 | def mimc_abi(exponent): 108 | assert exponent in (5, 7) 109 | return [{ 110 | "constant": True, 111 | "inputs": [ 112 | { 113 | "name": "in_x", 114 | "type": "uint256" 115 | }, 116 | { 117 | "name": "in_k", 118 | "type": "uint256" 119 | } 120 | ], 121 | "name": f"MiMCpe{exponent}", 122 | "outputs": [ 123 | { 124 | "name": "out_x", 125 | "type": "uint256" 126 | } 127 | ], 128 | "payable": False, 129 | "stateMutability": "pure", 130 | "type": "function" 131 | }] 132 | 133 | 134 | def mimc_contract(exponent): 135 | gen = Codegen() 136 | for _ in mimc_contract_opcodes(exponent): 137 | gen.append(_) 138 | return gen.createTxData() 139 | 140 | 141 | def main(*args): 142 | if len(args) < 3: 143 | print("Usage: %s [outfile]" % (args[0],)) 144 | return 1 145 | 146 | command = args[1] 147 | exponent = int(args[2]) 148 | if exponent not in (5, 7): 149 | print("Error: exponent must be 5 or 7") 150 | return 2 151 | 152 | outfile = sys.stdout 153 | if len(args) > 3: 154 | outfile = open(args[3], 'wb') 155 | 156 | if command == "abi": 157 | outfile.write(json.dumps(mimc_abi(exponent)) + "\n") 158 | elif command == "contract": 159 | data = mimc_contract(exponent) 160 | if outfile == sys.stdout: 161 | data = '0x' + hexlify(data).decode('ascii') 162 | outfile.write(data) 163 | else: 164 | print("Error: unknown command", command) 165 | 166 | if outfile != sys.stdout: 167 | outfile.close() 168 | 169 | return 0 170 | 171 | 172 | if __name__ == "__main__": 173 | sys.exit(main(*sys.argv)) 174 | -------------------------------------------------------------------------------- /sdk/ethsnarks/mimc/contract_sol.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from math import log2 3 | 4 | from ..field import SNARK_SCALAR_FIELD 5 | 6 | from .permutation import mimc_constants 7 | 8 | 9 | def mimc_contract_solidity(exponent, constants): 10 | # This means we can do 3 additions in sequence, before modulo reduction 11 | # Essentially replacing most of the `ADDMOD` instructions with `ADD` 12 | assert (3*(SNARK_SCALAR_FIELD-1)) < ((1<<256)-1) 13 | 14 | yield "pragma solidity ^0.5.0;" 15 | yield "library MiMCpe%d_generated {" % (exponent,) 16 | yield "\tfunction MiMCpe%d (uint256 in_x, uint256 in_k) internal pure returns (uint256 out_x) {" % (exponent,) 17 | yield "\t\tassembly {" 18 | yield f"\t\t\tlet localQ := {hex(SNARK_SCALAR_FIELD)}" 19 | yield "\t\t\tlet t" 20 | yield "\t\t\tlet a" 21 | 22 | for c_i in constants: 23 | c_i = c_i % SNARK_SCALAR_FIELD 24 | if exponent == 7: 25 | yield f"\t\t\tt := add(add(in_x, {hex(c_i)}), in_k)" # t = x + c_i + k 26 | yield "\t\t\ta := mulmod(t, t, localQ)" # t^2 27 | yield "\t\t\tin_x := mulmod(mulmod(a, mulmod(a, a, localQ), localQ), t, localQ)" # t^7 28 | elif exponent == 5: 29 | yield f"\t\t\tt := add(add(in_x, {hex(c_i)}), in_k)" # t = x + c_i + k 30 | yield "\t\t\ta := mulmod(t, t, localQ)" # t^2 31 | yield "\t\t\tin_x := mulmod(mulmod(a, a, localQ), t, localQ)" # t^5 32 | 33 | yield "\t\t\tout_x := addmod(in_x, in_k, localQ)" 34 | 35 | yield "\t\t}" 36 | yield "\t}" 37 | yield "}" 38 | 39 | 40 | def main(*args): 41 | if len(args) < 2: 42 | print("Usage: %s [outfile]" % (args[0],)) 43 | return 1 44 | 45 | exponent = int(args[1]) 46 | if exponent not in (5, 7): 47 | print("Error: exponent must be 5 or 7") 48 | return 2 49 | 50 | outfile = sys.stdout 51 | if len(args) > 2: 52 | outfile = open(args[3], 'wb') 53 | 54 | constants = mimc_constants(R=110 if exponent == 5 else 91) 55 | 56 | for line in mimc_contract_solidity(exponent, constants): 57 | outfile.write(line + "\n") 58 | 59 | if outfile != sys.stdout: 60 | outfile.close() 61 | 62 | return 0 63 | 64 | 65 | if __name__ == "__main__": 66 | sys.exit(main(*sys.argv)) 67 | -------------------------------------------------------------------------------- /sdk/ethsnarks/mimc/permutation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 HarryR 2 | # License: LGPL-3.0+ 3 | 4 | from ..sha3 import keccak_256 5 | from ..field import SNARK_SCALAR_FIELD 6 | 7 | 8 | DEFAULT_EXPONENT = 7 9 | DEFAULT_ROUNDS = 91 10 | DEFAULT_SEED = b'mimc' 11 | 12 | 13 | def to_bytes(*args): 14 | for i, _ in enumerate(args): 15 | if isinstance(_, str): 16 | yield _.encode('ascii') 17 | elif not isinstance(_, int) and hasattr(_, 'to_bytes'): 18 | # for 'F_p' or 'FQ' class etc. 19 | yield _.to_bytes('big') 20 | elif isinstance(_, bytes): 21 | yield _ 22 | else: 23 | # Try conversion to integer first? 24 | yield int(_).to_bytes(32, 'big') 25 | 26 | 27 | def H(*args): 28 | data = b''.join(to_bytes(*args)) 29 | hashed = keccak_256(data).digest() 30 | return int.from_bytes(hashed, 'big') 31 | 32 | assert H(123) == 38632140595220392354280998614525578145353818029287874088356304829962854601866 33 | 34 | 35 | def mimc_constants(seed=DEFAULT_SEED, p=SNARK_SCALAR_FIELD, R=DEFAULT_ROUNDS): 36 | """ 37 | Generate a sequence of round constants 38 | 39 | These can hard-coded into circuits or generated on-demand 40 | """ 41 | if isinstance(seed, str): 42 | seed = seed.encode('ascii') 43 | if isinstance(seed, bytes): 44 | # pre-hash byte strings before use 45 | seed = H(seed) 46 | else: 47 | seed = int(seed) 48 | 49 | for _ in range(R): 50 | seed = H(seed) 51 | yield seed 52 | 53 | 54 | def mimc(x, k, seed=DEFAULT_SEED, p=SNARK_SCALAR_FIELD, e=DEFAULT_EXPONENT, R=DEFAULT_ROUNDS): 55 | """ 56 | The MiMC cipher: https://eprint.iacr.org/2016/492 57 | 58 | First round 59 | 60 | x k 61 | | | 62 | | | 63 | (+)---| X[0] = x + k 64 | | | 65 | C[0] --(+) | Y[0] = X[0] + C[0] 66 | | | 67 | (n^7) | Z[0] = Y[0]^7 68 | | | 69 | ***************************************** 70 | per-round | | 71 | | | 72 | (+)---| X[i] = Z[i-1] + k 73 | | | 74 | C[i] --(+) | Y[i] = X[i] + C[i] 75 | | | 76 | (n^7) | Z[i] = Y[i]^7 77 | | | 78 | ***************************************** 79 | Last round 80 | | | 81 | (+)---' result = Z.back() + k 82 | | 83 | result 84 | """ 85 | assert R > 2 86 | # TODO: assert gcd(p-1, e) == 1 87 | for c_i in list(mimc_constants(seed, p, R)): 88 | a = (x + k + c_i) % p 89 | x = (a ** e) % p 90 | return (x + k) % p 91 | 92 | 93 | def mimc_hash(x, k=0, seed=DEFAULT_SEED, p=SNARK_SCALAR_FIELD, e=DEFAULT_EXPONENT, R=DEFAULT_ROUNDS): 94 | """ 95 | The Miyaguchi–Preneel single-block-length one-way compression 96 | function is an extended variant of Matyas–Meyer–Oseas. It was 97 | independently proposed by Shoji Miyaguchi and Bart Preneel. 98 | 99 | H_i = E_{H_{i-1}}(m_i) + {H_{i-1}} + m_i 100 | 101 | The previous output is used as the key for 102 | the next iteration. 103 | 104 | or.. 105 | 106 | m_i 107 | | 108 | |----, 109 | | | 110 | v | 111 | H_{i-1}--,-->[E] | 112 | | | | 113 | `-->(+)<--' 114 | | 115 | v 116 | H_i 117 | 118 | @param x list of inputs 119 | @param k initial key 120 | """ 121 | for x_i in x: 122 | r = mimc(x_i, k, seed, p, e, R) 123 | k = (k + x_i + r) % p 124 | return k 125 | 126 | 127 | def mimc_hash_md(x, k=0, seed=DEFAULT_SEED, p=SNARK_SCALAR_FIELD, e=DEFAULT_EXPONENT, R=DEFAULT_ROUNDS): 128 | """ 129 | Merkle-Damgard structure, used to turn a cipher into a one-way-compression function 130 | 131 | m_i 132 | | 133 | | 134 | v 135 | k_{i-1} -->[E] 136 | | 137 | | 138 | v 139 | k_i 140 | 141 | The output is used as the key for the next message 142 | The last output is used as the result 143 | """ 144 | for x_i in x: 145 | k = mimc(x_i, k, seed, p, e, R) 146 | return k 147 | 148 | 149 | def _main(): 150 | import argparse 151 | parser = argparse.ArgumentParser("MiMC") 152 | parser.add_argument('-r', '--rounds', metavar='N', type=int, default=DEFAULT_ROUNDS, help='number of rounds') 153 | parser.add_argument('-e', '--exponent', metavar='N', type=int, default=DEFAULT_EXPONENT, help='exponent for round function') 154 | parser.add_argument('-s', '--seed', type=bytes, default=DEFAULT_SEED, help='seed for round constants') 155 | parser.add_argument('-k', '--key', type=int, default=0, help='initial key') 156 | parser.add_argument('-v', '--verbose', action='store_true', default=False, help='display settings') 157 | parser.add_argument('cmd', nargs='?', default='test') 158 | parser.add_argument('subargs', nargs='*') 159 | args = parser.parse_args() 160 | 161 | exponent = args.exponent 162 | rounds = args.rounds 163 | seed = args.seed 164 | key = int(args.key) 165 | cmd = args.cmd 166 | 167 | if args.verbose: 168 | print('# exponent', exponent) 169 | print('# rounds', rounds) 170 | print('# seed', seed) 171 | print('# key', key) 172 | 173 | if cmd == "test": 174 | # With default parameters, known results 175 | assert mimc(1, 1) == 2447343676970420247355835473667983267115132689045447905848734383579598297563 176 | assert mimc_hash([1,1]) == 4087330248547221366577133490880315793780387749595119806283278576811074525767 177 | 178 | # Verify cross-compatibility with EVM/Solidity implementation 179 | assert mimc(3703141493535563179657531719960160174296085208671919316200479060314459804651, 180 | 134551314051432487569247388144051420116740427803855572138106146683954151557, 181 | b'mimc') == 11437467823393790387399137249441941313717686441929791910070352316474327319704 182 | assert mimc_hash([3703141493535563179657531719960160174296085208671919316200479060314459804651, 183 | 134551314051432487569247388144051420116740427803855572138106146683954151557], 184 | 918403109389145570117360101535982733651217667914747213867238065296420114726, 185 | b'mimc') == 15683951496311901749339509118960676303290224812129752890706581988986633412003 186 | print('OK') 187 | return 0 188 | 189 | elif cmd == "constants": 190 | for x in mimc_constants(seed, SNARK_SCALAR_FIELD, rounds): 191 | print(x % SNARK_SCALAR_FIELD) # hex(x), x) 192 | 193 | elif cmd == "encrypt": 194 | for x in args.subargs: 195 | x = int(x) 196 | result = mimc(x, key, seed, SNARK_SCALAR_FIELD, exponent, rounds) 197 | key = mimc(key, key, seed, SNARK_SCALAR_FIELD, exponent, rounds) 198 | print(result) 199 | 200 | elif cmd == "hash": 201 | result = mimc_hash([int(x) for x in args.subargs], key, seed, SNARK_SCALAR_FIELD, exponent, rounds) 202 | print(result) 203 | 204 | else: 205 | parser.print_help() 206 | return 1 207 | 208 | return 0 209 | 210 | 211 | if __name__ == "__main__": 212 | import sys 213 | sys.exit(_main()) 214 | -------------------------------------------------------------------------------- /sdk/ethsnarks/numbertheory.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # Provide some simple capabilities from number theory. 4 | # 5 | # Version of 2008.11.14. 6 | # 7 | # Written in 2005 and 2006 by Peter Pearson and placed in the public domain. 8 | # Revision history: 9 | # 2008.11.14: Use pow( base, exponent, modulus ) for modular_exp. 10 | # Make gcd and lcm accept arbitrarly many arguments. 11 | 12 | from __future__ import division, print_function 13 | 14 | import sys 15 | import math 16 | from functools import reduce 17 | 18 | if sys.version_info.major > 2: 19 | integer_types = (int,) 20 | long = int 21 | else: 22 | integer_types = (int, long) # noqa: F821 23 | 24 | 25 | class Error( Exception ): 26 | """Base class for exceptions in this module.""" 27 | pass 28 | 29 | class SquareRootError( Error ): 30 | pass 31 | 32 | class NegativeExponentError( Error ): 33 | pass 34 | 35 | 36 | def modular_exp( base, exponent, modulus ): 37 | "Raise base to exponent, reducing by modulus" 38 | if exponent < 0: 39 | raise NegativeExponentError( "Negative exponents (%d) not allowed" \ 40 | % exponent ) 41 | return pow( base, exponent, modulus ) 42 | # result = 1L 43 | # x = exponent 44 | # b = base + 0L 45 | # while x > 0: 46 | # if x % 2 > 0: result = (result * b) % modulus 47 | # x = x // 2 48 | # b = ( b * b ) % modulus 49 | # return result 50 | 51 | 52 | def polynomial_reduce_mod( poly, polymod, p ): 53 | """Reduce poly by polymod, integer arithmetic modulo p. 54 | 55 | Polynomials are represented as lists of coefficients 56 | of increasing powers of x.""" 57 | 58 | # This module has been tested only by extensive use 59 | # in calculating modular square roots. 60 | 61 | # Just to make this easy, require a monic polynomial: 62 | assert polymod[-1] == 1 63 | 64 | assert len( polymod ) > 1 65 | 66 | while len( poly ) >= len( polymod ): 67 | if poly[-1] != 0: 68 | for i in range( 2, len( polymod ) + 1 ): 69 | poly[-i] = ( poly[-i] - poly[-1] * polymod[-i] ) % p 70 | poly = poly[0:-1] 71 | 72 | return poly 73 | 74 | 75 | 76 | def polynomial_multiply_mod( m1, m2, polymod, p ): 77 | """Polynomial multiplication modulo a polynomial over ints mod p. 78 | 79 | Polynomials are represented as lists of coefficients 80 | of increasing powers of x.""" 81 | 82 | # This is just a seat-of-the-pants implementation. 83 | 84 | # This module has been tested only by extensive use 85 | # in calculating modular square roots. 86 | 87 | # Initialize the product to zero: 88 | 89 | prod = ( len( m1 ) + len( m2 ) - 1 ) * [0] 90 | 91 | # Add together all the cross-terms: 92 | 93 | for i in range( len( m1 ) ): 94 | for j in range( len( m2 ) ): 95 | prod[i+j] = ( prod[i+j] + m1[i] * m2[j] ) % p 96 | 97 | return polynomial_reduce_mod( prod, polymod, p ) 98 | 99 | 100 | def polynomial_exp_mod( base, exponent, polymod, p ): 101 | """Polynomial exponentiation modulo a polynomial over ints mod p. 102 | 103 | Polynomials are represented as lists of coefficients 104 | of increasing powers of x.""" 105 | 106 | # Based on the Handbook of Applied Cryptography, algorithm 2.227. 107 | 108 | # This module has been tested only by extensive use 109 | # in calculating modular square roots. 110 | 111 | assert exponent < p 112 | 113 | if exponent == 0: return [ 1 ] 114 | 115 | G = base 116 | k = exponent 117 | if k%2 == 1: s = G 118 | else: s = [ 1 ] 119 | 120 | while k > 1: 121 | k = k // 2 122 | G = polynomial_multiply_mod( G, G, polymod, p ) 123 | if k%2 == 1: s = polynomial_multiply_mod( G, s, polymod, p ) 124 | 125 | return s 126 | 127 | 128 | 129 | def jacobi( a, n ): 130 | """Jacobi symbol""" 131 | 132 | # Based on the Handbook of Applied Cryptography (HAC), algorithm 2.149. 133 | 134 | # This function has been tested by comparison with a small 135 | # table printed in HAC, and by extensive use in calculating 136 | # modular square roots. 137 | 138 | assert n >= 3 139 | assert n%2 == 1 140 | a = a % n 141 | if a == 0: return 0 142 | if a == 1: return 1 143 | a1, e = a, 0 144 | while a1%2 == 0: 145 | a1, e = a1//2, e+1 146 | if e%2 == 0 or n%8 == 1 or n%8 == 7: s = 1 147 | else: s = -1 148 | if a1 == 1: return s 149 | if n%4 == 3 and a1%4 == 3: s = -s 150 | return s * jacobi( n % a1, a1 ) 151 | 152 | 153 | 154 | def square_root_mod_prime( a, p ): 155 | """Modular square root of a, mod p, p prime.""" 156 | 157 | # Based on the Handbook of Applied Cryptography, algorithms 3.34 to 3.39. 158 | 159 | # This module has been tested for all values in [0,p-1] for 160 | # every prime p from 3 to 1229. 161 | 162 | assert 0 <= a < p 163 | assert 1 < p 164 | 165 | if a == 0: return 0 166 | if p == 2: return a 167 | 168 | jac = jacobi( a, p ) 169 | if jac == -1: raise SquareRootError( "%d has no square root modulo %d" \ 170 | % ( a, p ) ) 171 | 172 | if p % 4 == 3: return modular_exp( a, (p+1)//4, p ) 173 | 174 | if p % 8 == 5: 175 | d = modular_exp( a, (p-1)//4, p ) 176 | if d == 1: return modular_exp( a, (p+3)//8, p ) 177 | if d == p-1: return ( 2 * a * modular_exp( 4*a, (p-5)//8, p ) ) % p 178 | raise RuntimeError("Shouldn't get here.") 179 | 180 | for b in range( 2, p ): 181 | if jacobi( b*b-4*a, p ) == -1: 182 | f = ( a, -b, 1 ) 183 | ff = polynomial_exp_mod( ( 0, 1 ), (p+1)//2, f, p ) 184 | assert ff[1] == 0 185 | return ff[0] 186 | raise RuntimeError("No b found.") 187 | 188 | 189 | 190 | def inverse_mod( a, m ): 191 | """Inverse of a mod m.""" 192 | 193 | if a < 0 or m <= a: a = a % m 194 | 195 | # From Ferguson and Schneier, roughly: 196 | 197 | c, d = a, m 198 | uc, vc, ud, vd = 1, 0, 0, 1 199 | while c != 0: 200 | q, c, d = divmod( d, c ) + ( c, ) 201 | uc, vc, ud, vd = ud - q*uc, vd - q*vc, uc, vc 202 | 203 | # At this point, d is the GCD, and ud*a+vd*m = d. 204 | # If d == 1, this means that ud is a inverse. 205 | 206 | assert d == 1 207 | if ud > 0: return ud 208 | else: return ud + m 209 | 210 | 211 | def gcd2(a, b): 212 | """Greatest common divisor using Euclid's algorithm.""" 213 | while a: 214 | a, b = b%a, a 215 | return b 216 | 217 | 218 | def gcd( *a ): 219 | """Greatest common divisor. 220 | 221 | Usage: gcd( [ 2, 4, 6 ] ) 222 | or: gcd( 2, 4, 6 ) 223 | """ 224 | 225 | if len( a ) > 1: return reduce( gcd2, a ) 226 | if hasattr( a[0], "__iter__" ): return reduce( gcd2, a[0] ) 227 | return a[0] 228 | 229 | 230 | def lcm2(a,b): 231 | """Least common multiple of two integers.""" 232 | 233 | return (a*b)//gcd(a,b) 234 | 235 | 236 | def lcm( *a ): 237 | """Least common multiple. 238 | 239 | Usage: lcm( [ 3, 4, 5 ] ) 240 | or: lcm( 3, 4, 5 ) 241 | """ 242 | 243 | if len( a ) > 1: return reduce( lcm2, a ) 244 | if hasattr( a[0], "__iter__" ): return reduce( lcm2, a[0] ) 245 | return a[0] 246 | 247 | 248 | 249 | def factorization( n ): 250 | """Decompose n into a list of (prime,exponent) pairs.""" 251 | 252 | assert isinstance( n, integer_types ) 253 | 254 | if n < 2: return [] 255 | 256 | result = [] 257 | d = 2 258 | 259 | # Test the small primes: 260 | 261 | for d in smallprimes: 262 | if d > n: break 263 | q, r = divmod( n, d ) 264 | if r == 0: 265 | count = 1 266 | while d <= n: 267 | n = q 268 | q, r = divmod( n, d ) 269 | if r != 0: break 270 | count = count + 1 271 | result.append( ( d, count ) ) 272 | 273 | # If n is still greater than the last of our small primes, 274 | # it may require further work: 275 | 276 | if n > smallprimes[-1]: 277 | if is_prime( n ): # If what's left is prime, it's easy: 278 | result.append( ( n, 1 ) ) 279 | else: # Ugh. Search stupidly for a divisor: 280 | d = smallprimes[-1] 281 | while 1: 282 | d = d + 2 # Try the next divisor. 283 | q, r = divmod( n, d ) 284 | if q < d: break # n < d*d means we're done, n = 1 or prime. 285 | if r == 0: # d divides n. How many times? 286 | count = 1 287 | n = q 288 | while d <= n: # As long as d might still divide n, 289 | q, r = divmod( n, d ) # see if it does. 290 | if r != 0: break 291 | n = q # It does. Reduce n, increase count. 292 | count = count + 1 293 | result.append( ( d, count ) ) 294 | if n > 1: result.append( ( n, 1 ) ) 295 | 296 | return result 297 | 298 | 299 | 300 | def phi( n ): 301 | """Return the Euler totient function of n.""" 302 | 303 | assert isinstance( n, integer_types ) 304 | 305 | if n < 3: return 1 306 | 307 | result = 1 308 | ff = factorization( n ) 309 | for f in ff: 310 | e = f[1] 311 | if e > 1: 312 | result = result * f[0] ** (e-1) * ( f[0] - 1 ) 313 | else: 314 | result = result * ( f[0] - 1 ) 315 | return result 316 | 317 | 318 | def carmichael( n ): 319 | """Return Carmichael function of n. 320 | 321 | Carmichael(n) is the smallest integer x such that 322 | m**x = 1 mod n for all m relatively prime to n. 323 | """ 324 | 325 | return carmichael_of_factorized( factorization( n ) ) 326 | 327 | 328 | def carmichael_of_factorized( f_list ): 329 | """Return the Carmichael function of a number that is 330 | represented as a list of (prime,exponent) pairs. 331 | """ 332 | 333 | if len( f_list ) < 1: return 1 334 | 335 | result = carmichael_of_ppower( f_list[0] ) 336 | for i in range( 1, len( f_list ) ): 337 | result = lcm( result, carmichael_of_ppower( f_list[i] ) ) 338 | 339 | return result 340 | 341 | def carmichael_of_ppower( pp ): 342 | """Carmichael function of the given power of the given prime. 343 | """ 344 | 345 | p, a = pp 346 | if p == 2 and a > 2: return 2**(a-2) 347 | else: return (p-1) * p**(a-1) 348 | 349 | 350 | 351 | def order_mod( x, m ): 352 | """Return the order of x in the multiplicative group mod m. 353 | """ 354 | 355 | # Warning: this implementation is not very clever, and will 356 | # take a long time if m is very large. 357 | 358 | if m <= 1: return 0 359 | 360 | assert gcd( x, m ) == 1 361 | 362 | z = x 363 | result = 1 364 | while z != 1: 365 | z = ( z * x ) % m 366 | result = result + 1 367 | return result 368 | 369 | 370 | def largest_factor_relatively_prime( a, b ): 371 | """Return the largest factor of a relatively prime to b. 372 | """ 373 | 374 | while 1: 375 | d = gcd( a, b ) 376 | if d <= 1: break 377 | b = d 378 | while 1: 379 | q, r = divmod( a, d ) 380 | if r > 0: 381 | break 382 | a = q 383 | return a 384 | 385 | 386 | def kinda_order_mod( x, m ): 387 | """Return the order of x in the multiplicative group mod m', 388 | where m' is the largest factor of m relatively prime to x. 389 | """ 390 | 391 | return order_mod( x, largest_factor_relatively_prime( m, x ) ) 392 | 393 | 394 | def is_prime( n ): 395 | """Return True if x is prime, False otherwise. 396 | 397 | We use the Miller-Rabin test, as given in Menezes et al. p. 138. 398 | This test is not exact: there are composite values n for which 399 | it returns True. 400 | 401 | In testing the odd numbers from 10000001 to 19999999, 402 | about 66 composites got past the first test, 403 | 5 got past the second test, and none got past the third. 404 | Since factors of 2, 3, 5, 7, and 11 were detected during 405 | preliminary screening, the number of numbers tested by 406 | Miller-Rabin was (19999999 - 10000001)*(2/3)*(4/5)*(6/7) 407 | = 4.57 million. 408 | """ 409 | 410 | # (This is used to study the risk of false positives:) 411 | global miller_rabin_test_count 412 | 413 | miller_rabin_test_count = 0 414 | 415 | if n <= smallprimes[-1]: 416 | if n in smallprimes: return True 417 | else: return False 418 | 419 | if gcd( n, 2*3*5*7*11 ) != 1: return False 420 | 421 | # Choose a number of iterations sufficient to reduce the 422 | # probability of accepting a composite below 2**-80 423 | # (from Menezes et al. Table 4.4): 424 | 425 | t = 40 426 | n_bits = 1 + int( math.log( n, 2 ) ) 427 | for k, tt in ( ( 100, 27 ), 428 | ( 150, 18 ), 429 | ( 200, 15 ), 430 | ( 250, 12 ), 431 | ( 300, 9 ), 432 | ( 350, 8 ), 433 | ( 400, 7 ), 434 | ( 450, 6 ), 435 | ( 550, 5 ), 436 | ( 650, 4 ), 437 | ( 850, 3 ), 438 | ( 1300, 2 ), 439 | ): 440 | if n_bits < k: break 441 | t = tt 442 | 443 | # Run the test t times: 444 | 445 | s = 0 446 | r = n - 1 447 | while ( r % 2 ) == 0: 448 | s = s + 1 449 | r = r // 2 450 | for i in range( t ): 451 | a = smallprimes[ i ] 452 | y = modular_exp( a, r, n ) 453 | if y != 1 and y != n-1: 454 | j = 1 455 | while j <= s - 1 and y != n - 1: 456 | y = modular_exp( y, 2, n ) 457 | if y == 1: 458 | miller_rabin_test_count = i + 1 459 | return False 460 | j = j + 1 461 | if y != n-1: 462 | miller_rabin_test_count = i + 1 463 | return False 464 | return True 465 | 466 | 467 | def next_prime( starting_value ): 468 | "Return the smallest prime larger than the starting value." 469 | 470 | if starting_value < 2: return 2 471 | result = ( starting_value + 1 ) | 1 472 | while not is_prime( result ): result = result + 2 473 | return result 474 | 475 | 476 | smallprimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 477 | 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 478 | 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 479 | 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 480 | 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 481 | 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 482 | 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 483 | 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 484 | 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 485 | 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 486 | 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 487 | 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 488 | 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 489 | 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 490 | 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 491 | 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 492 | 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 493 | 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 494 | 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 495 | 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229] 496 | 497 | miller_rabin_test_count = 0 498 | 499 | def __main__(): 500 | 501 | # Making sure locally defined exceptions work: 502 | # p = modular_exp( 2, -2, 3 ) 503 | # p = square_root_mod_prime( 2, 3 ) 504 | 505 | 506 | print("Testing gcd...") 507 | assert gcd( 3*5*7, 3*5*11, 3*5*13 ) == 3*5 508 | assert gcd( [ 3*5*7, 3*5*11, 3*5*13 ] ) == 3*5 509 | assert gcd( 3 ) == 3 510 | 511 | print("Testing lcm...") 512 | assert lcm( 3, 5*3, 7*3 ) == 3*5*7 513 | assert lcm( [ 3, 5*3, 7*3 ] ) == 3*5*7 514 | assert lcm( 3 ) == 3 515 | 516 | print("Testing next_prime...") 517 | bigprimes = ( 999671, 518 | 999683, 519 | 999721, 520 | 999727, 521 | 999749, 522 | 999763, 523 | 999769, 524 | 999773, 525 | 999809, 526 | 999853, 527 | 999863, 528 | 999883, 529 | 999907, 530 | 999917, 531 | 999931, 532 | 999953, 533 | 999959, 534 | 999961, 535 | 999979, 536 | 999983 ) 537 | 538 | for i in range( len( bigprimes ) - 1 ): 539 | assert next_prime( bigprimes[i] ) == bigprimes[ i+1 ] 540 | 541 | error_tally = 0 542 | 543 | # Test the square_root_mod_prime function: 544 | 545 | for p in smallprimes: 546 | print("Testing square_root_mod_prime for modulus p = %d." % p) 547 | squares = [] 548 | 549 | for root in range( 0, 1+p//2 ): 550 | sq = ( root * root ) % p 551 | squares.append( sq ) 552 | calculated = square_root_mod_prime( sq, p ) 553 | if ( calculated * calculated ) % p != sq: 554 | error_tally = error_tally + 1 555 | print("Failed to find %d as sqrt( %d ) mod %d. Said %d." % \ 556 | ( root, sq, p, calculated )) 557 | 558 | for nonsquare in range( 0, p ): 559 | if nonsquare not in squares: 560 | try: 561 | calculated = square_root_mod_prime( nonsquare, p ) 562 | except SquareRootError: 563 | pass 564 | else: 565 | error_tally = error_tally + 1 566 | print("Failed to report no root for sqrt( %d ) mod %d." % \ 567 | ( nonsquare, p )) 568 | 569 | # Test the jacobi function: 570 | for m in range( 3, 400, 2 ): 571 | print("Testing jacobi for modulus m = %d." % m) 572 | if is_prime( m ): 573 | squares = [] 574 | for root in range( 1, m ): 575 | if jacobi( root * root, m ) != 1: 576 | error_tally = error_tally + 1 577 | print("jacobi( %d * %d, %d ) != 1" % ( root, root, m )) 578 | squares.append( root * root % m ) 579 | for i in range( 1, m ): 580 | if not i in squares: 581 | if jacobi( i, m ) != -1: 582 | error_tally = error_tally + 1 583 | print("jacobi( %d, %d ) != -1" % ( i, m )) 584 | else: # m is not prime. 585 | f = factorization( m ) 586 | for a in range( 1, m ): 587 | c = 1 588 | for i in f: 589 | c = c * jacobi( a, i[0] ) ** i[1] 590 | if c != jacobi( a, m ): 591 | error_tally = error_tally + 1 592 | print("%d != jacobi( %d, %d )" % ( c, a, m )) 593 | 594 | 595 | # Test the inverse_mod function: 596 | print("Testing inverse_mod . . .") 597 | import random 598 | n_tests = 0 599 | for i in range( 100 ): 600 | m = random.randint( 20, 10000 ) 601 | for j in range( 100 ): 602 | a = random.randint( 1, m-1 ) 603 | if gcd( a, m ) == 1: 604 | n_tests = n_tests + 1 605 | inv = inverse_mod( a, m ) 606 | if inv <= 0 or inv >= m or ( a * inv ) % m != 1: 607 | error_tally = error_tally + 1 608 | print("%d = inverse_mod( %d, %d ) is wrong." % ( inv, a, m )) 609 | assert n_tests > 1000 610 | print(n_tests, " tests of inverse_mod completed.") 611 | 612 | class FailedTest(Exception): pass 613 | print(error_tally, "errors detected.") 614 | if error_tally != 0: 615 | raise FailedTest("%d errors detected" % error_tally) 616 | 617 | if __name__ == '__main__': 618 | __main__() -------------------------------------------------------------------------------- /sdk/ethsnarks/pedersen.py: -------------------------------------------------------------------------------- 1 | import math 2 | import bitstring 3 | from math import floor, log2 4 | from struct import pack 5 | 6 | from .jubjub import Point, EtecPoint, JUBJUB_L, JUBJUB_C 7 | 8 | 9 | MAX_SEGMENT_BITS = floor(log2(JUBJUB_L)) 10 | MAX_SEGMENT_BYTES = MAX_SEGMENT_BITS // 8 11 | 12 | 13 | def pedersen_hash_basepoint(name, i): 14 | """ 15 | Create a base point for use with the windowed pedersen hash function. 16 | The name and sequence numbers are used a unique identifier. 17 | Then HashToPoint is run on the name+seq to get the base point. 18 | """ 19 | if not isinstance(name, bytes): 20 | if isinstance(name, str): 21 | name = name.encode('ascii') 22 | else: 23 | raise TypeError("Name not bytes") 24 | if i < 0 or i > 0xFFFF: 25 | raise ValueError("Sequence number invalid") 26 | if len(name) > 28: 27 | raise ValueError("Name too long") 28 | data = b"%-28s%04X" % (name, i) 29 | return Point.from_hash(data).as_etec() 30 | 31 | 32 | def pedersen_hash_windows(name, windows): 33 | # 62 is defined in the ZCash Sapling Specification, Theorem 5.4.1 34 | # See: https://github.com/HarryR/ethsnarks/issues/121#issuecomment-499441289 35 | n_windows = 62 36 | result = EtecPoint.infinity() 37 | for j, window in enumerate(windows): 38 | if j % n_windows == 0: 39 | current = pedersen_hash_basepoint(name, j//n_windows) 40 | j = j % n_windows 41 | if j != 0: 42 | current = current.double().double().double().double() 43 | segment = current * ((window & 0b11) + 1) 44 | if window > 0b11: 45 | segment = segment.neg() 46 | result += segment 47 | return result.as_point() 48 | 49 | 50 | def pedersen_hash_bits(name, bits): 51 | # Split into 3 bit windows 52 | if isinstance(bits, bitstring.BitArray): 53 | bits = bits.bin 54 | windows = [int(bits[i:i+3][::-1], 2) for i in range(0, len(bits), 3)] 55 | assert len(windows) > 0 56 | 57 | # Hash resulting windows 58 | return pedersen_hash_windows(name, windows) 59 | 60 | 61 | def pedersen_hash_bytes(name, data): 62 | """ 63 | Hashes a sequence of bits (the message) into a point. 64 | 65 | The message is split into 3-bit windows after padding (via append) 66 | to `len(data.bits) = 0 mod 3` 67 | """ 68 | assert isinstance(data, bytes) 69 | assert len(data) > 0 70 | 71 | # Decode bytes to octets of binary bits 72 | bits = ''.join([bin(_)[2:].rjust(8, '0') for _ in data]) 73 | 74 | return pedersen_hash_bits(name, bits) 75 | 76 | 77 | def pedersen_hash_scalars(name, *scalars): 78 | """ 79 | Calculates a pedersen hash of scalars in the same way that zCash 80 | is doing it according to: ... of their spec. 81 | It is looking up 3bit chunks in a 2bit table (3rd bit denotes sign). 82 | 83 | E.g: 84 | 85 | (b2, b1, b0) = (1,0,1) would look up first element and negate it. 86 | 87 | Row i of the lookup table contains: 88 | 89 | [2**4i * base, 2 * 2**4i * base, 3 * 2**4i * base, 3 * 2**4i * base] 90 | 91 | E.g: 92 | 93 | row_0 = [base, 2*base, 3*base, 4*base] 94 | row_1 = [16*base, 32*base, 48*base, 64*base] 95 | row_2 = [256*base, 512*base, 768*base, 1024*base] 96 | 97 | Following Theorem 5.4.1 of the zCash Sapling specification, for baby jub_jub 98 | we need a new base point every 62 windows. We will therefore have multiple 99 | tables with 62 rows each. 100 | """ 101 | windows = [] 102 | for i, s in enumerate(scalars): 103 | windows += list((s >> i) & 0b111 for i in range(0,s.bit_length(),3)) 104 | return pedersen_hash_windows(name, windows) 105 | -------------------------------------------------------------------------------- /sdk/ethsnarks/poseidon/__init__.py: -------------------------------------------------------------------------------- 1 | from .permutation import * 2 | 3 | if __name__ == "__main__": 4 | # TODO: implement 'constants' and 'matrix' 5 | pass 6 | -------------------------------------------------------------------------------- /sdk/ethsnarks/poseidon/contract.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 Jordi Baylina 2 | # Copyright (c) 2019 Harry Roberts 3 | # License: LGPL-3.0+ 4 | 5 | 6 | import sys 7 | import json 8 | from binascii import hexlify 9 | from ..evmasm import * 10 | from ..field import SNARK_SCALAR_FIELD 11 | from .permutation import DefaultParams 12 | 13 | 14 | def _add_round_key(r, t, K): 15 | """ 16 | function ark(r) { 17 | C.push(toHex256(K[r])); // K, st, q 18 | for (let i=0; i=nRoundsP+nRoundsF/2)) { 199 | for (let j=0; j= (params.nRoundsP+(params.nRoundsF//2)): 216 | for j in range(params.t): 217 | yield _sigma(j, params.t) 218 | else: 219 | yield _sigma(0, params.t) 220 | label = 'after_mix_%d' % (i,) 221 | yield [ 222 | PUSHLABEL(label), 223 | PUSH(0), 224 | MSTORE, 225 | JMP('mix'), 226 | LABEL(label) 227 | ] 228 | 229 | """ 230 | C.push("0x00"); 231 | C.mstore(); // Save it to pos 0; 232 | C.push("0x20"); 233 | C.push("0x00"); 234 | C.return(); 235 | mix(); 236 | """ 237 | yield [PUSH(0), 238 | MSTORE, # Save it to pos 0 239 | PUSH(0x20), 240 | PUSH(0), 241 | RETURN] 242 | 243 | for _ in _mix(params): 244 | yield _ 245 | 246 | 247 | def poseidon_contract(params=None): 248 | gen = Codegen() 249 | for _ in poseidon_contract_opcodes(params): 250 | gen.append(_) 251 | return gen.createTxData() 252 | 253 | 254 | def poseidon_abi(): 255 | return [ 256 | { 257 | "constant": True, 258 | "inputs": [ 259 | { 260 | "name": "input", 261 | "type": "uint256[]" 262 | } 263 | ], 264 | "name": "poseidon", 265 | "outputs": [ 266 | { 267 | "name": "", 268 | "type": "uint256" 269 | } 270 | ], 271 | "payable": False, 272 | "stateMutability": "pure", 273 | "type": "function" 274 | } 275 | ] 276 | 277 | 278 | def main(*args): 279 | if len(args) < 2: 280 | print("Usage: %s [outfile]" % (args[0],)) 281 | return 1 282 | command = args[1] 283 | outfile = sys.stdout 284 | if len(args) > 2: 285 | outfile = open(args[2], 'wb') 286 | if command == "abi": 287 | outfile.write(json.dumps(poseidon_abi()) + "\n") 288 | return 0 289 | elif command == "contract": 290 | data = poseidon_contract() 291 | if outfile == sys.stdout: 292 | data = '0x' + hexlify(data).decode('ascii') 293 | outfile.write(data) 294 | return 0 295 | else: 296 | print("Error: unknown command", command) 297 | if outfile != sys.stdout: 298 | outfile.close() 299 | 300 | if __name__ == "__main__": 301 | sys.exit(main(*sys.argv)) 302 | -------------------------------------------------------------------------------- /sdk/ethsnarks/poseidon/permutation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Implements the Poseidon permutation: 5 | 6 | Starkad and Poseidon: New Hash Functions for Zero Knowledge Proof Systems 7 | - Lorenzo Grassi, Daniel Kales, Dmitry Khovratovich, Arnab Roy, Christian Rechberger, and Markus Schofnegger 8 | - https://eprint.iacr.org/2019/458.pdf 9 | 10 | Other implementations: 11 | 12 | - https://github.com/shamatar/PoseidonTree/ 13 | - https://github.com/iden3/circomlib/blob/master/src/poseidon.js 14 | - https://github.com/dusk-network/poseidon252 15 | """ 16 | 17 | from math import log2, floor 18 | from collections import namedtuple 19 | from pyblake2 import blake2b 20 | from ..field import SNARK_SCALAR_FIELD 21 | 22 | 23 | PoseidonParamsType = namedtuple('_PoseidonParams', ('p', 't', 'nRoundsF', 'nRoundsP', 'seed', 'e', 'constants_C', 'constants_M')) 24 | 25 | 26 | def poseidon_params(p, t, nRoundsF, nRoundsP, seed, e, constants_C=None, constants_M=None, security_target=None): 27 | assert nRoundsF % 2 == 0 and nRoundsF > 0 28 | assert nRoundsP > 0 29 | assert t >= 2 30 | assert isinstance(seed, bytes) 31 | 32 | n = floor(log2(p)) 33 | if security_target is None: 34 | M = n # security target, in bits 35 | else: 36 | M = security_target 37 | assert n >= M 38 | 39 | # Size of the state (in bits) 40 | N = n * t 41 | 42 | if p % 2 == 3: 43 | assert e == 3 44 | grobner_attack_ratio_rounds = 0.32 45 | grobner_attack_ratio_sboxes = 0.18 46 | interpolation_attack_ratio = 0.63 47 | elif p % 5 != 1: 48 | assert e == 5 49 | grobner_attack_ratio_rounds = 0.21 50 | grobner_attack_ratio_sboxes = 0.14 51 | interpolation_attack_ratio = 0.43 52 | else: 53 | # XXX: in other cases use, can we use 7? 54 | raise ValueError('Invalid p for congruency') 55 | 56 | # Verify that the parameter choice exceeds the recommendations to prevent attacks 57 | # iacr.org/2019/458 § 3 Cryptanalysis Summary of Starkad and Poseidon Hashes (pg 10) 58 | # Figure 1 59 | #print('(nRoundsF + nRoundsP)', (nRoundsF + nRoundsP)) 60 | #print('Interpolation Attackable Rounds', ((interpolation_attack_ratio * min(n, M)) + log2(t))) 61 | assert (nRoundsF + nRoundsP) > ((interpolation_attack_ratio * min(n, M)) + log2(t)) 62 | # Figure 3 63 | #print('grobner_attack_ratio_rounds', ((2 + min(M, n)) * grobner_attack_ratio_rounds)) 64 | assert (nRoundsF + nRoundsP) > ((2 + min(M, n)) * grobner_attack_ratio_rounds) 65 | # Figure 4 66 | #print('grobner_attack_ratio_sboxes', (M * grobner_attack_ratio_sboxes)) 67 | assert (nRoundsF + (t * nRoundsP)) > (M * grobner_attack_ratio_sboxes) 68 | 69 | # iacr.org/2019/458 § 4.1 Minimize "Number of S-Boxes" 70 | # In order to minimize the number of S-boxes for given `n` and `t`, the goal is to and 71 | # the best ratio between RP and RF that minimizes: 72 | # number of S-Boxes = t · RF + RP 73 | # - Use S-box x^q 74 | # - Select R_F to 6 or rhigher 75 | # - Select R_P that minimizes tRF +RP such that no inequation (1),(3),(4),(5) is satisfied. 76 | 77 | if constants_C is None: 78 | constants_C = list(poseidon_constants(p, seed + b'_constants', nRoundsF + nRoundsP)) 79 | if constants_M is None: 80 | constants_M = poseidon_matrix(p, seed + b'_matrix_0000', t) 81 | 82 | # iacr.org/2019/458 § 4.1 6 SNARKs Application via Poseidon-π 83 | # page 16 formula (8) and (9) 84 | n_constraints = (nRoundsF * t) + nRoundsP 85 | if e == 5: 86 | n_constraints *= 3 87 | elif e == 3: 88 | n_constraints *= 2 89 | #print('n_constraints', n_constraints) 90 | 91 | return PoseidonParamsType(p, t, nRoundsF, nRoundsP, seed, e, constants_C, constants_M) 92 | 93 | 94 | def H(arg): 95 | if isinstance(arg, int): 96 | arg = arg.to_bytes(32, 'little') 97 | # XXX: ensure that (digest_size*8) >= log2(p) 98 | hashed = blake2b(data=arg, digest_size=32).digest() 99 | return int.from_bytes(hashed, 'little') 100 | 101 | 102 | def poseidon_constants(p, seed, n): 103 | assert isinstance(n, int) 104 | for _ in range(n): 105 | seed = H(seed) 106 | yield seed % p 107 | 108 | 109 | def poseidon_matrix(p, seed, t): 110 | """ 111 | iacr.org/2019/458 § 2.3 About the MDS Matrix (pg 8) 112 | Also: 113 | - https://en.wikipedia.org/wiki/Cauchy_matrix 114 | """ 115 | c = list(poseidon_constants(p, seed, t * 2)) 116 | return [[pow((c[i] - c[t+j]) % p, p - 2, p) for j in range(t)] 117 | for i in range(t)] 118 | 119 | 120 | DefaultParams = poseidon_params(SNARK_SCALAR_FIELD, 6, 8, 57, b'poseidon', 5, security_target=126) 121 | 122 | 123 | def poseidon_sbox(state, i, params): 124 | """ 125 | iacr.org/2019/458 § 2.2 The Hades Strategy (pg 6) 126 | 127 | In more details, assume R_F = 2 · R_f is an even number. Then 128 | - the first R_f rounds have a full S-Box layer, 129 | - the middle R_P rounds have a partial S-Box layer (i.e., 1 S-Box layer), 130 | - the last R_f rounds have a full S-Box layer 131 | """ 132 | half_F = params.nRoundsF // 2 133 | e, p = params.e, params.p 134 | if i < half_F or i >= (half_F + params.nRoundsP): 135 | for j, _ in enumerate(state): 136 | state[j] = pow(_, e, p) 137 | else: 138 | state[0] = pow(state[0], e, p) 139 | 140 | 141 | def poseidon_mix(state, M, p): 142 | """ 143 | The mixing layer is a matrix vector product of the state with the mixing matrix 144 | - https://mathinsight.org/matrix_vector_multiplication 145 | """ 146 | return [ sum([M[i][j] * _ for j, _ in enumerate(state)]) % p 147 | for i in range(len(M)) ] 148 | 149 | 150 | def poseidon(inputs, params=None, chained=False, trace=False): 151 | """ 152 | Main instansiation of the Poseidon permutation 153 | 154 | The state is `t` elements wide, there are `F` full-rounds 155 | followed by `P` partial rounds, then `F` full rounds again. 156 | 157 | [ ARK ] --, 158 | | | | | | | | 159 | [ SBOX ] - Full Round 160 | | | | | | | | 161 | [ MIX ] --` 162 | 163 | 164 | [ ARK ] --, 165 | | | | | | | | 166 | [ SBOX ] - Partial Round 167 | | | Only 1 element is substituted in partial round 168 | [ MIX ] --` 169 | 170 | There are F+P rounds for the full permutation. 171 | 172 | You can provide `r = N - 2s` bits of input per round, where `s` is the desired 173 | security level, in most cases this means you can provide `t-1` inputs with 174 | appropriately chosen parameters. The permutation can be 'chained' together 175 | to form a sponge construct. 176 | """ 177 | if params is None: 178 | params = DefaultParams 179 | assert isinstance(params, PoseidonParamsType) 180 | assert len(inputs) > 0 181 | if not chained: 182 | # Don't allow inputs to exceed the rate, unless in chained mode 183 | assert len(inputs) < params.t 184 | state = [0] * params.t 185 | state[:len(inputs)] = inputs 186 | for i, C_i in enumerate(params.constants_C): 187 | state = [_ + C_i for _ in state] # ARK(.) 188 | poseidon_sbox(state, i, params) 189 | state = poseidon_mix(state, params.constants_M, params.p) 190 | if trace: 191 | for j, val in enumerate(state): 192 | print('%d %d' % (i, j), '=', val) 193 | if chained: 194 | # Provide the full state as output in 'chained' mode 195 | return state 196 | return state[0] 197 | -------------------------------------------------------------------------------- /sdk/ethsnarks/r1cs.py: -------------------------------------------------------------------------------- 1 | from .field import FQ 2 | 3 | def r1cs_constraint(a, b, c): 4 | if not isinstance(a, FQ): 5 | a = FQ(a) 6 | if not isinstance(b, FQ): 7 | b = FQ(b) 8 | if not isinstance(c, FQ): 9 | c = FQ(c) 10 | if not a * b == c: 11 | raise RuntimeError("R1CS Constraint Failed!") 12 | -------------------------------------------------------------------------------- /sdk/ethsnarks/sha3.py: -------------------------------------------------------------------------------- 1 | try: 2 | # pysha3 3 | from sha3 import keccak_256 4 | except ImportError: 5 | # pycryptodome 6 | from Crypto.Hash import keccak 7 | keccak_256 = lambda *args: keccak.new(*args, digest_bits=256) 8 | -------------------------------------------------------------------------------- /sdk/ethsnarks/shamirspoly.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 HarryR 2 | # License: LGPL-3.0+ 3 | 4 | from .field import FQ 5 | 6 | 7 | def shamirs_poly(x, a): 8 | assert isinstance(a, (list,tuple)) 9 | assert len(a) >= 2 10 | assert isinstance(x, FQ) 11 | 12 | result = a[0] 13 | x_pow_i = x 14 | 15 | for i, a_i in list(enumerate(a))[1:]: 16 | assert isinstance(a_i, FQ) 17 | ai_mul_xi = a_i * x_pow_i 18 | result = result + ai_mul_xi 19 | x_pow_i *= x 20 | 21 | return result 22 | 23 | 24 | def lagrange(points, x): 25 | # Borrowed from: https://gist.github.com/melpomene/2482930 26 | total = 0 27 | n = len(points) 28 | for i in range(n): 29 | xi, yi = points[i] 30 | assert isinstance(xi, FQ) 31 | assert isinstance(yi, FQ) 32 | def g(i, n): 33 | tot_mul = 1 34 | for j in range(n): 35 | if i == j: 36 | continue 37 | xj, yj = points[j] 38 | tot_mul = tot_mul * ( (x - xj) // (xi - xj) ) 39 | return tot_mul 40 | coefficient = g(i, n) 41 | total = total + (yi * coefficient) 42 | return total 43 | 44 | 45 | def inverse_lagrange(points, y): 46 | x = 0 47 | for i, (x_i, y_i) in enumerate(points): 48 | for j, (x_j, y_j) in enumerate(points): 49 | if j != i: 50 | x_i = x_i * (y - y_j) / (y_i - y_j) 51 | x += x_i 52 | return x 53 | -------------------------------------------------------------------------------- /sdk/ethsnarks/utils.py: -------------------------------------------------------------------------------- 1 | 2 | import platform 3 | 4 | 5 | def native_lib_path(libname): 6 | # get the right filename 7 | if platform.uname()[0] == "Windows": 8 | extn = ".dll" 9 | if platform.uname()[0] == "Darwin": 10 | extn = ".dylib" 11 | else: 12 | extn = ".so" 13 | return libname + extn 14 | 15 | 16 | def bytes_to_field_elements(in_bytes, chunk_size=253): 17 | assert isinstance(in_bytes, bytes) 18 | as_bits = ''.join([bin(_)[2:].rjust(8, '0') for _ in in_bytes]) 19 | num_bits = len(as_bits) 20 | chunks = [as_bits[_:_+chunk_size][::-1] for _ in range(0, num_bits, chunk_size)] 21 | return [int(_, 2) for _ in chunks] 22 | 23 | 24 | def libsnark2python (inputs): 25 | bin_inputs = [] 26 | for x in inputs: 27 | binary = bin(x)[2:][::-1] 28 | if len(binary) > 100: 29 | binary = binary.ljust(253, "0") 30 | bin_inputs.append(binary) 31 | raw = "".join(bin_inputs) 32 | 33 | raw += "0" * (256 * 5 - len(raw)) 34 | 35 | output = [] 36 | i = 0 37 | while i < len(raw): 38 | hexnum = hex(int(raw[i:i+256], 2)) 39 | #pad leading zeros 40 | padding = 66 - len(hexnum) 41 | hexnum = hexnum[:2] + "0"*padding + hexnum[2:] 42 | 43 | output.append(hexnum) 44 | i += 256 45 | return(output) 46 | -------------------------------------------------------------------------------- /sdk/ethsnarks/verifier.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018 HarryR. 2 | # License: LGPL-3.0+ 3 | 4 | 5 | import json 6 | import ctypes 7 | from functools import reduce 8 | from binascii import unhexlify 9 | from collections import namedtuple 10 | 11 | from py_ecc import bn128 12 | from py_ecc.bn128 import pairing, FQ, FQ2, FQ12, neg, multiply, add 13 | 14 | 15 | _VerifyingKeyStruct = namedtuple('_VerifyingKeyStruct', 16 | ('alpha', 'beta', 'gamma', 'delta', 'gammaABC')) 17 | 18 | _ProofStruct = namedtuple('_ProofStruct', 19 | ('A', 'B', 'C', 'input')) 20 | 21 | 22 | class CustomEncoder(json.JSONEncoder): 23 | """Encodes FQ and FQ2 elements in same format as native library and Ethereum""" 24 | def default(self, o): 25 | if isinstance(o, FQ2): 26 | c0hex = hex(o.coeffs[0].n) 27 | c1hex = hex(o.coeffs[1].n) 28 | return [c1hex, c0hex] 29 | elif isinstance(o, FQ): 30 | return hex(o.n) 31 | raise RuntimeError("Unknown type", (type(o), o)) 32 | 33 | 34 | def _bigint_bytes_to_int(x): 35 | """Convert big-endian bytes to integer""" 36 | return reduce(lambda o, b: (o << 8) + b if isinstance(b, int) else ord(b), [0] + list(x)) 37 | 38 | 39 | def _filter_int(x): 40 | """Decode an optionally hex-encoded big-endian string to a integer""" 41 | if isinstance(x, int): 42 | return x 43 | if x[:2] == '0x': 44 | x = x[2:] 45 | if len(x) % 2 > 0: 46 | x = '0' + x 47 | return _bigint_bytes_to_int(unhexlify(x)) 48 | 49 | 50 | def _load_g1_point(point): 51 | """Unserialize a G1 point, from Ethereum hex encoded 0x...""" 52 | if len(point) != 2: 53 | raise RuntimeError("Invalid G1 point - not 2 vals", point) 54 | 55 | out = tuple(FQ(_filter_int(_)) for _ in point) 56 | 57 | if not bn128.is_on_curve(out, bn128.b): 58 | raise ValueError("Invalid G1 point - not on curve", out) 59 | 60 | return out 61 | 62 | 63 | def _load_g2_point(point): 64 | """Unserialize a G2 point, from Ethereum hex encoded 0x...""" 65 | x, y = point 66 | if len(x) != 2 or len(y) != 2: 67 | raise RuntimeError("Invalid G2 point x or y", point) 68 | 69 | # Points are provided as X.c1, X.c0, Y.c1, Y.c2 70 | # As in, each component is a 512 bit big-endian number split in two 71 | out = (FQ2([_filter_int(x[1]), _filter_int(x[0])]), 72 | FQ2([_filter_int(y[1]), _filter_int(y[0])])) 73 | 74 | if not bn128.is_on_curve(out, bn128.b2): 75 | raise ValueError("Invalid G2 point - not on curve:", out) 76 | 77 | # TODO: verify G2 point with another algorithm? 78 | # neg(G2.one()) * p + p != G2.zero() 79 | return out 80 | 81 | 82 | def pairingProd(*inputs): 83 | """ 84 | The Ethereum pairing opcode works like: 85 | 86 | e(p1[0],p2[0]) * ... * e(p1[n],p2[n]) == 1 87 | 88 | See: EIP 212 89 | 90 | >>> assert True == pairingProd((G1, G2), (G1, neg(G2))) 91 | """ 92 | product = FQ12.one() 93 | for p1, p2 in inputs: 94 | product *= pairing(p2, p1) 95 | return product == FQ12.one() 96 | 97 | 98 | class BaseProof(object): 99 | def to_json(self): 100 | obj = self._asdict() 101 | # Note, the inputs must be hex-encoded so they're JSON friendly 102 | # The Custom JSON encoder doesn't handle them correctly 103 | for field in self.FP_POINTS: 104 | obj[field] = [hex(_) for _ in obj[field]] 105 | return json.dumps(obj, cls=CustomEncoder) 106 | 107 | @classmethod 108 | def from_json(cls, json_data): 109 | return cls.from_dict(json.loads(json_data)) 110 | 111 | @classmethod 112 | def from_dict(cls, in_data): 113 | """ 114 | The G1 points in the proof JSON are affine X,Y,Z coordinates 115 | Because they're affine we can ignore the Z coordinate 116 | 117 | For G2 points on-chain, they're: X.c1, X.c0, Y.c1, Y.c0, Z.c1, Z.c0 118 | 119 | However, py_ecc is little endian, so it needs [X.c0, X.c1] 120 | """ 121 | fields = [] 122 | for name in cls._fields: 123 | val = in_data[name] 124 | if name in cls.G2_POINTS: 125 | # See note above about endian conversion 126 | fields.append(_load_g2_point(val)) 127 | elif name in cls.FP_POINTS: 128 | fields.append([_filter_int(_) for _ in val]) 129 | elif name in cls.G1_POINTS: 130 | fields.append(_load_g1_point(val[:2])) 131 | else: 132 | raise KeyError("Unknown proof field: " + name) 133 | return cls(*fields) 134 | 135 | 136 | class Proof(_ProofStruct, BaseProof): 137 | G1_POINTS = ['A', 'C'] 138 | G2_POINTS = ['B'] 139 | FP_POINTS = ['input'] 140 | 141 | 142 | class BaseVerifier(object): 143 | def to_json(self): 144 | return json.dumps(self._asdict(), cls=CustomEncoder) 145 | 146 | @classmethod 147 | def from_json(cls, json_data): 148 | return cls.from_dict(json.loads(json_data)) 149 | 150 | @classmethod 151 | def from_file(cls, filename): 152 | with open(filename, 'r') as handle: 153 | data = json.load(handle) 154 | return cls.from_dict(data) 155 | 156 | @classmethod 157 | def from_dict(cls, in_data): 158 | """Load verifying key from data dictionary, e.g. 'vk.json'""" 159 | fields = [] 160 | for name in cls._fields: 161 | val = in_data[name] 162 | # Iterate in order, loading G1 or G2 points as necessary 163 | if name in cls.G1_POINTS: 164 | fields.append(_load_g1_point(val)) 165 | elif name in cls.G1_LISTS: 166 | fields.append(list([_load_g1_point(_) for _ in val])) 167 | elif name in cls.G2_POINTS: 168 | fields.append(_load_g2_point(val)) 169 | else: 170 | raise KeyError("Unknown verifying key field: " + name) 171 | # Order is necessary to pass to constructor of self 172 | return cls(*fields) 173 | 174 | 175 | class VerifyingKey(BaseVerifier, _VerifyingKeyStruct): 176 | G1_POINTS = ['alpha'] 177 | G2_POINTS = ['beta', 'gamma', 'delta'] 178 | G1_LISTS = ['gammaABC'] 179 | 180 | def verify(self, proof): 181 | """Verify if a proof is correct for the given inputs""" 182 | if not isinstance(proof, Proof): 183 | raise TypeError("Invalid proof type") 184 | 185 | # Compute the linear combination vk_x 186 | # vk_x = gammaABC[0] + gammaABC[1]^x[0] + ... + gammaABC[n+1]^x[n] 187 | vk_x = self.gammaABC[0] 188 | for i, x in enumerate(proof.input): 189 | vk_x = add(vk_x, multiply(self.gammaABC[i + 1], x)) 190 | 191 | # e(B, A) * e(gamma, -vk_x) * e(delta, -C) * e(beta, -alpha) 192 | return pairingProd( 193 | (proof.A, proof.B), 194 | (neg(vk_x), self.gamma), 195 | (neg(proof.C), self.delta), 196 | (neg(self.alpha), self.beta)) 197 | 198 | 199 | class NativeVerifier(VerifyingKey): 200 | def verify(self, proof, native_library_path): 201 | if not isinstance(proof, Proof): 202 | raise TypeError("Invalid proof type") 203 | 204 | vk_cstr = ctypes.c_char_p(self.to_json().encode('ascii')) 205 | proof_cstr = ctypes.c_char_p(proof.to_json().encode('ascii')) 206 | 207 | lib = ctypes.cdll.LoadLibrary(native_library_path) 208 | lib_verify = lib.ethsnarks_verify 209 | lib_verify.argtypes = [ctypes.c_char_p, ctypes.c_char_p] 210 | lib_verify.restype = ctypes.c_bool 211 | 212 | return lib_verify(vk_cstr, proof_cstr) 213 | -------------------------------------------------------------------------------- /sdk/request_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/sdk/request_utils/__init__.py -------------------------------------------------------------------------------- /sdk/request_utils/rest_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | from datetime import datetime 4 | from enum import Enum 5 | from multiprocessing.dummy import Pool 6 | from queue import Empty, Queue 7 | from typing import Any, Callable, Optional, Union 8 | 9 | import requests 10 | import time 11 | 12 | 13 | class RequestStatus(Enum): 14 | ready = 0 # Request created 15 | success = 1 # Request successful (status code 2xx) 16 | failed = 2 # Request failed (status code not 2xx) 17 | error = 3 # Exception raised 18 | 19 | 20 | class Request(object): 21 | """ 22 | Request object for status check. 23 | """ 24 | 25 | def __init__( 26 | self, 27 | method: str, 28 | path: str, 29 | params: dict, 30 | data: Union[dict, str, bytes], 31 | headers: dict, 32 | callback: Callable = None, 33 | on_failed: Callable = None, 34 | on_error: Callable = None, 35 | extra: Any = None, 36 | ): 37 | """""" 38 | self.method = method 39 | self.path = path 40 | self.callback = callback 41 | self.params = params 42 | self.data = data 43 | self.headers = headers 44 | 45 | self.on_failed = on_failed 46 | self.on_error = on_error 47 | self.extra = extra 48 | 49 | self.response = None 50 | self.status = RequestStatus.ready 51 | 52 | def __str__(self): 53 | if self.response is None: 54 | status_code = "terminated" 55 | else: 56 | status_code = self.response.status_code 57 | 58 | return ( 59 | "request : {} {} {} because {}: \n" 60 | "headers: {}\n" 61 | "params: {}\n" 62 | "data: {}\n" 63 | "response:" 64 | "{}\n".format( 65 | self.method, 66 | self.path, 67 | self.status.name, 68 | status_code, 69 | self.headers, 70 | self.params, 71 | self.data, 72 | "" if self.response is None else self.response.text, 73 | ) 74 | ) 75 | 76 | 77 | class RestClient(object): 78 | """ 79 | HTTP Client designed for all sorts of trading RESTFul API. 80 | 81 | * Reimplement sign function to add signature function. 82 | * Reimplement on_failed function to handle Non-2xx responses. 83 | * Use on_failed parameter in add_request function for individual Non-2xx response handling. 84 | * Reimplement on_error function to handle exception msg. 85 | """ 86 | 87 | def __init__(self): 88 | """ 89 | """ 90 | self.url_base = '' # type: str 91 | self._active = False 92 | 93 | self._queue = Queue() 94 | self._pool = None # type: Pool 95 | 96 | self.proxies = None 97 | 98 | def init(self, url_base: str, proxy_host: str = "", proxy_port: int = 0): 99 | """ 100 | Init rest client with url_base which is the API root address. 101 | """ 102 | self.url_base = url_base 103 | 104 | if proxy_host and proxy_port: 105 | proxy = f"{proxy_host}:{proxy_port}" 106 | self.proxies = {"http": proxy, "https": proxy} 107 | 108 | def _create_session(self): 109 | """""" 110 | return requests.session() 111 | 112 | def start(self, n: int = 3): 113 | """ 114 | Start rest client with session count n. 115 | """ 116 | if self._active: 117 | return 118 | 119 | self._active = True 120 | self._pool = Pool(n) 121 | self._pool.apply_async(self._run) 122 | 123 | def stop(self): 124 | """ 125 | Stop rest client immediately. 126 | """ 127 | self._active = False 128 | 129 | def join(self): 130 | """ 131 | Wait till all requests are processed. 132 | """ 133 | self._queue.join() 134 | 135 | def add_request( 136 | self, 137 | method: str, 138 | path: str, 139 | callback: Callable, 140 | params: dict = None, 141 | data: Union[dict, str, bytes] = None, 142 | headers: dict = None, 143 | on_failed: Callable = None, 144 | on_error: Callable = None, 145 | extra: Any = None, 146 | ): 147 | """ 148 | Add a new request. 149 | :param method: GET, POST, PUT, DELETE, QUERY 150 | :param path: 151 | :param callback: callback function if 2xx status, type: (dict, Request) 152 | :param params: dict for query string 153 | :param data: Http body. If it is a dict, it will be converted to form-data. Otherwise, it will be converted to bytes. 154 | :param headers: dict for headers 155 | :param on_failed: callback function if Non-2xx status, type, type: (code, dict, Request) 156 | :param on_error: callback function when catching Python exception, type: (etype, evalue, tb, Request) 157 | :param extra: Any extra data which can be used when handling callback 158 | :return: Request 159 | """ 160 | request = Request( 161 | method, 162 | path, 163 | params, 164 | data, 165 | headers, 166 | callback, 167 | on_failed, 168 | on_error, 169 | extra, 170 | ) 171 | self._queue.put(request) 172 | return request 173 | 174 | def _run(self): 175 | try: 176 | session = self._create_session() 177 | while self._active: 178 | try: 179 | request = self._queue.get(timeout=1) 180 | try: 181 | self._process_request(request, session) 182 | finally: 183 | self._queue.task_done() 184 | except Empty: 185 | pass 186 | except Exception: 187 | et, ev, tb = sys.exc_info() 188 | self.on_error(et, ev, tb, None) 189 | 190 | def sign(self, request: Request): 191 | """ 192 | This function is called before sending any request out. 193 | Please implement signature method here. 194 | @:return (request) 195 | """ 196 | return request 197 | 198 | def on_failed(self, status_code: int, request: Request): 199 | """ 200 | Default on_failed handler for Non-2xx response. 201 | """ 202 | sys.stderr.write(str(request)) 203 | 204 | def on_error( 205 | self, 206 | exception_type: type, 207 | exception_value: Exception, 208 | tb, 209 | request: Optional[Request], 210 | ): 211 | """ 212 | Default on_error handler for Python exception. 213 | """ 214 | sys.stderr.write( 215 | self.exception_detail(exception_type, exception_value, tb, request) 216 | ) 217 | sys.excepthook(exception_type, exception_value, tb) 218 | 219 | def exception_detail( 220 | self, 221 | exception_type: type, 222 | exception_value: Exception, 223 | tb, 224 | request: Optional[Request], 225 | ): 226 | text = "[{}]: Unhandled RestClient Error:{}\n".format( 227 | datetime.now().isoformat(), exception_type 228 | ) 229 | text += "request:{}\n".format(request) 230 | text += "Exception trace: \n" 231 | text += "".join( 232 | traceback.format_exception(exception_type, exception_value, tb) 233 | ) 234 | return text 235 | 236 | def _process_request( 237 | self, request: Request, session: requests.Session 238 | ): 239 | """ 240 | Sending request to server and get result. 241 | """ 242 | try: 243 | request = self.sign(request) 244 | url = self.make_full_url(request.path) 245 | response = session.request( 246 | request.method, 247 | url, 248 | headers=request.headers, 249 | params=request.params, 250 | data=request.data, 251 | proxies=self.proxies, 252 | ) 253 | request.response = response 254 | status_code = response.status_code 255 | if status_code // 100 == 2: # 2xx codes are all successful 256 | if status_code == 204: 257 | json_body = None 258 | else: 259 | json_body = response.json() 260 | 261 | request.callback(json_body, request) 262 | request.status = RequestStatus.success 263 | else: 264 | request.status = RequestStatus.failed 265 | 266 | if request.on_failed: 267 | request.on_failed(status_code, request) 268 | else: 269 | self.on_failed(status_code, request) 270 | except Exception: 271 | request.status = RequestStatus.error 272 | t, v, tb = sys.exc_info() 273 | if request.on_error: 274 | request.on_error(t, v, tb, request) 275 | else: 276 | self.on_error(t, v, tb, request) 277 | 278 | def make_full_url(self, path: str): 279 | """ 280 | Make relative api path into full url. 281 | eg: make_full_url('/get') == 'http://xxxxx/get' 282 | """ 283 | url = self.url_base + path 284 | return url 285 | 286 | def request( 287 | self, 288 | method: str, 289 | path: str, 290 | params: dict = None, 291 | data: dict = None, 292 | headers: dict = None, 293 | ): 294 | """ 295 | Add a new request. 296 | :param method: GET, POST, PUT, DELETE, QUERY 297 | :param path: 298 | :param params: dict for query string 299 | :param data: dict for body 300 | :param headers: dict for headers 301 | :return: requests.Response 302 | """ 303 | request = Request( 304 | method, 305 | path, 306 | params, 307 | data, 308 | headers 309 | ) 310 | request = self.sign(request) 311 | 312 | url = self.make_full_url(request.path) 313 | 314 | response = requests.request( 315 | request.method, 316 | url, 317 | headers=request.headers, 318 | params=request.params, 319 | data=request.data, 320 | proxies=self.proxies, 321 | ) 322 | return response 323 | -------------------------------------------------------------------------------- /sdk/sig_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/sdk/sig_utils/__init__.py -------------------------------------------------------------------------------- /sdk/sig_utils/ecdsa_utils.py: -------------------------------------------------------------------------------- 1 | import eip712_structs 2 | from eip712_structs import EIP712Struct, Address, Array, Boolean, Bytes, Int, String, Uint 3 | from eth_abi import encode_single, encode_abi 4 | from py_eth_sig_utils import utils as sig_utils 5 | from web3 import Web3 6 | from sdk.ethsnarks.jubjub import Point 7 | from sdk.ethsnarks.field import FQ 8 | 9 | 10 | class EIP712: 11 | EIP191_HEADER = bytes.fromhex("1901") 12 | 13 | ammPoolDomains = {} 14 | 15 | @classmethod 16 | def init_env(cls, name, version, chainId, verifyingContract): 17 | cls.exchangeDomain = eip712_structs.make_domain(name = name, 18 | version=version, 19 | chainId=chainId, 20 | verifyingContract=verifyingContract) 21 | # eip712_structs.default_domain = cls.exchangeDomain 22 | 23 | @classmethod 24 | def init_amm_env(cls, name, version, chainId, verifyingContract): 25 | cls.ammPoolDomains[verifyingContract] = eip712_structs.make_domain(name = name, 26 | version=version, 27 | chainId=chainId, 28 | verifyingContract=verifyingContract) 29 | # eip712_structs.default_domain = cls.exchangeDomain 30 | 31 | 32 | @classmethod 33 | def hash_packed(cls, domainHash, dataHash): 34 | # print(f"domainHash = {bytes.hex(domainHash)}") 35 | # print(f"dataHash = {bytes.hex(dataHash)}") 36 | return Web3.keccak( 37 | b''.join([ 38 | cls.EIP191_HEADER, 39 | domainHash, 40 | dataHash 41 | ]) 42 | ) 43 | 44 | # EIP712.init_env() 45 | 46 | def generateUpdateAccountEIP712Hash(req: dict): 47 | class AccountUpdate(EIP712Struct): 48 | owner = Address() 49 | accountID = Uint(32) 50 | feeTokenID = Uint(16) 51 | maxFee = Uint(96) 52 | publicKey = Uint(256) 53 | validUntil = Uint(32) 54 | nonce = Uint(32) 55 | 56 | pt = Point(FQ(int(req['publicKey']['x'], 16)), FQ(int(req['publicKey']['y'], 16))) 57 | publicKey = int.from_bytes(pt.compress(), "little") 58 | # print(f"publicKey = {publicKey}") 59 | 60 | # "AccountUpdate(address owner,uint32 accountID,uint16 feeTokenID,uint96 maxFee,uint256 publicKey,uint32 validUntil,uint32 nonce)" 61 | update = AccountUpdate( 62 | owner = req['owner'], #bytes.fromhex(req['owner'].replace("0x", "")), 63 | accountID = req['accountId'], 64 | feeTokenID = req['maxFee']['tokenId'], 65 | maxFee = int(req['maxFee']['volume']), 66 | publicKey = publicKey, 67 | validUntil = req['validUntil'], 68 | nonce = req['nonce'] 69 | ) 70 | 71 | # print(f"update type hash = {bytes.hex(update.type_hash())}") 72 | return EIP712.hash_packed( 73 | EIP712.exchangeDomain.hash_struct(), 74 | update.hash_struct() 75 | ) 76 | 77 | def generateTransferEIP712Hash(req: dict): 78 | """ 79 | struct Transfer 80 | { 81 | address from; 82 | address to; 83 | uint16 tokenID; 84 | uint amount; 85 | uint16 feeTokenID; 86 | uint fee; 87 | uint32 validUntil; 88 | uint32 storageID; 89 | } 90 | """ 91 | class Transfer(EIP712Struct): 92 | pass 93 | 94 | setattr(Transfer, 'from', Address()) 95 | Transfer.to = Address() 96 | Transfer.tokenID = Uint(16) 97 | Transfer.amount = Uint(96) 98 | Transfer.feeTokenID = Uint(16) 99 | Transfer.maxFee = Uint(96) 100 | Transfer.validUntil = Uint(32) 101 | Transfer.storageID = Uint(32) 102 | 103 | # "Transfer(address from,address to,uint16 tokenID,uint96 amount,uint16 feeTokenID,uint96 maxFee,uint32 validUntil,uint32 storageID)" 104 | transfer = Transfer(**{ 105 | "from" : req['payerAddr'], 106 | "to" : req['payeeAddr'], 107 | "tokenID" : req['token']['tokenId'], 108 | "amount" : int(req['token']['volume']), 109 | "feeTokenID" : req['maxFee']['tokenId'], 110 | "maxFee" : int(req['maxFee']['volume']), 111 | "validUntil" : req['validUntil'], 112 | "storageID" : req['storageId'] 113 | }) 114 | 115 | # print(f"transfer type hash = {bytes.hex(transfer.type_hash())}") 116 | return EIP712.hash_packed( 117 | EIP712.exchangeDomain.hash_struct(), 118 | transfer.hash_struct() 119 | ) 120 | 121 | def generateOffchainWithdrawalEIP712Hash(req: dict): 122 | """ 123 | struct Withdrawal 124 | { 125 | address owner; 126 | uint32 accountID; 127 | uint16 tokenID; 128 | uint amount; 129 | uint16 feeTokenID; 130 | uint fee; 131 | address to; 132 | bytes32 extraDataHash; 133 | uint minGas; 134 | uint32 validUntil; 135 | uint32 storageID; 136 | } 137 | 138 | """ 139 | class Withdrawal(EIP712Struct): 140 | owner = Address() 141 | accountID = Uint(32) 142 | tokenID = Uint(16) 143 | amount = Uint(96) 144 | feeTokenID = Uint(16) 145 | maxFee = Uint(96) 146 | to = Address() 147 | extraData = Bytes() 148 | minGas = Uint() 149 | validUntil = Uint(32) 150 | storageID = Uint(32) 151 | 152 | # "Withdrawal(address owner,uint32 accountID,uint16 tokenID,uint96 amount,uint16 feeTokenID,uint96 maxFee,address to,bytes extraData,uint256 minGas,uint32 validUntil,uint32 storageID)" 153 | withdrawal = Withdrawal(**{ 154 | "owner" : req['owner'], 155 | "accountID" : req['accountId'], 156 | "tokenID" : req['token']['tokenId'], 157 | "amount" : int(req['token']['volume']), 158 | "feeTokenID" : req['maxFee']['tokenId'], 159 | "maxFee" : int(req['maxFee']['volume']), 160 | "to" : req['to'], 161 | "extraData" : bytes.fromhex(req['extraData']), 162 | "minGas" : int(req['minGas']), 163 | "validUntil" : req['validUntil'], 164 | "storageID" : req['storageId'], 165 | }) 166 | 167 | # print(f"extraData hash = {bytes.hex(Web3.keccak(bytes.fromhex(req['extraData'])))}") 168 | # print(f"withdrawal type hash = {bytes.hex(withdrawal.type_hash())}") 169 | return EIP712.hash_packed( 170 | EIP712.exchangeDomain.hash_struct(), 171 | withdrawal.hash_struct() 172 | ) 173 | 174 | def generateAmmPoolJoinEIP712Hash(req: dict): 175 | """ 176 | struct PoolJoin 177 | { 178 | address owner; 179 | uint96[] joinAmounts; 180 | uint32[] joinStorageIDs; 181 | uint96 mintMinAmount; 182 | uint32 validUntil; 183 | } 184 | """ 185 | class PoolJoin(EIP712Struct): 186 | owner = Address() 187 | joinAmounts = Array(Uint(96)) 188 | joinStorageIDs = Array(Uint(32)) 189 | mintMinAmount = Uint(96) 190 | validUntil = Uint(32) 191 | 192 | # "PoolJoin(address owner,uint96[] joinAmounts,uint32[] joinStorageIDs,uint96 mintMinAmount,uint32 validUntil)" 193 | join = PoolJoin( 194 | owner = req['owner'], 195 | joinAmounts = [int(token['volume']) for token in req['joinTokens']['pooled']], 196 | joinStorageIDs = [int(id) for id in req['storageIds']], 197 | mintMinAmount = int(req['joinTokens']['minimumLp']['volume']), 198 | validUntil = req['validUntil'] 199 | ) 200 | 201 | # print(f"PoolJoin type hash = {bytes.hex(join.type_hash())}") 202 | return EIP712.hash_packed( 203 | EIP712.ammPoolDomains[req['poolAddress']].hash_struct(), 204 | join.hash_struct() 205 | ) 206 | 207 | def generateAmmPoolExitEIP712Hash(req: dict): 208 | """ 209 | struct PoolExit 210 | { 211 | address owner; 212 | uint96 burnAmount; 213 | uint32 burnStorageID; // for pool token withdrawal from user to the pool 214 | uint96[] exitMinAmounts; 215 | uint96 fee; 216 | uint32 validUntil; 217 | } 218 | """ 219 | class PoolExit(EIP712Struct): 220 | owner = Address() 221 | burnAmount = Uint(96) 222 | burnStorageID = Uint(32) 223 | exitMinAmounts = Array(Uint(96)) 224 | fee = Uint(96) 225 | validUntil = Uint(32) 226 | 227 | # "PoolExit(address owner,uint96 burnAmount,uint32 burnStorageID,uint96[] exitMinAmounts,uint96 fee,uint32 validUntil)" 228 | exit = PoolExit( 229 | owner = req['owner'], 230 | burnAmount = int(req['exitTokens']['burned']['volume']), 231 | burnStorageID = int(req['storageId']), 232 | exitMinAmounts = [int(token['volume']) for token in req['exitTokens']['unPooled']], 233 | fee = int(req['maxFee']), 234 | validUntil = req['validUntil'] 235 | ) 236 | 237 | # print(f"PoolExit type hash = {bytes.hex(exit.type_hash())}") 238 | return EIP712.hash_packed( 239 | EIP712.ammPoolDomains[req['poolAddress']].hash_struct(), 240 | exit.hash_struct() 241 | ) 242 | 243 | import sys 244 | 245 | if __name__ == "__main__": 246 | EIP712.init_amm_env(name=sys.argv[2], 247 | version="1.0.0", 248 | chainId=sys.argv[1], 249 | verifyingContract=sys.argv[3]) 250 | domainSep = EIP712.ammPoolDomains[sys.argv[3]].hash_struct() 251 | print("0x" + bytes.hex(domainSep).zfill(64)) -------------------------------------------------------------------------------- /sdk/sig_utils/eddsa_utils.py: -------------------------------------------------------------------------------- 1 | from sdk.ethsnarks.eddsa import PureEdDSA, PoseidonEdDSA 2 | from sdk.ethsnarks.field import FQ, SNARK_SCALAR_FIELD 3 | from sdk.ethsnarks.poseidon import poseidon_params, poseidon 4 | from sdk.ethsnarks.eddsa import Signature, SignedMessage 5 | import urllib 6 | import hashlib 7 | import json 8 | 9 | class EddsaSignHelper: 10 | def __init__(self, poseidon_params, private_key = "0x1"): 11 | self.poseidon_sign_param = poseidon_params 12 | self.private_key = FQ(int(private_key, 16)) 13 | assert self.private_key != FQ.zero() 14 | # print(f"self.private_key = {self.private_key}") 15 | 16 | def hash(self, structure_data): 17 | serialized_data = self.serialize_data(structure_data) 18 | msgHash = poseidon(serialized_data, self.poseidon_sign_param) 19 | return msgHash 20 | 21 | def sign(self, structure_data): 22 | msgHash = self.hash(structure_data) 23 | signedMessage = PoseidonEdDSA.sign(msgHash, self.private_key) 24 | # print("sign=", signedMessage) 25 | return "0x" + "".join([ 26 | hex(int(signedMessage.sig.R.x))[2:].zfill(64), 27 | hex(int(signedMessage.sig.R.y))[2:].zfill(64), 28 | hex(int(signedMessage.sig.s))[2:].zfill(64) 29 | ]) 30 | 31 | def sigStrToSignature(self, sig): 32 | assert len(sig) == 194 33 | pureHexSig = sig[2:] 34 | return Signature( 35 | [ 36 | int(pureHexSig[:64], 16), 37 | int(pureHexSig[64:128], 16) 38 | ], 39 | int(pureHexSig[128:], 16) 40 | ) 41 | 42 | def serialize_data(self, data): 43 | pass 44 | 45 | def verify(self, message, sig): 46 | return PoseidonEdDSA.verify(sig.A, sig.sig, sig.msg) 47 | 48 | class DummyEddsaSignHelper(EddsaSignHelper): 49 | def __init__(self, private_key): 50 | super(DummyEddsaSignHelper, self).__init__( 51 | poseidon_params = poseidon_params(SNARK_SCALAR_FIELD, 2, 6, 53, b'poseidon', 5, security_target=128), 52 | private_key = private_key 53 | ) 54 | 55 | def serialize_data(self, dummy): 56 | return [ 57 | int(dummy["data"]), 58 | ] 59 | 60 | class UrlEddsaSignHelper(EddsaSignHelper): 61 | def __init__(self, private_key, host=""): 62 | self.host = host 63 | super(UrlEddsaSignHelper, self).__init__( 64 | poseidon_params = poseidon_params(SNARK_SCALAR_FIELD, 2, 6, 53, b'poseidon', 5, security_target=128), 65 | private_key = private_key 66 | ) 67 | 68 | def hash(self, structure_data): 69 | serialized_data = self.serialize_data(structure_data) 70 | hasher = hashlib.sha256() 71 | hasher.update(serialized_data.encode('utf-8')) 72 | msgHash = int(hasher.hexdigest(), 16) % SNARK_SCALAR_FIELD 73 | # print(f"serialized_data = {serialized_data}, prehash = {hasher.hexdigest()}, msgHash = {hex(msgHash)}") 74 | return msgHash 75 | 76 | def serialize_data(self, request): 77 | method = request.method 78 | url = urllib.parse.quote(self.host + request.path, safe='') 79 | if method in ["GET", "DELETE"]: 80 | data = urllib.parse.quote("&".join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in request.params.items()]), safe='') 81 | elif method in ["POST", "PUT"]: 82 | data = urllib.parse.quote(json.dumps(request.data, separators=(',', ':')), safe='') 83 | else: 84 | raise Exception(f"Unknown request method {method}") 85 | 86 | # return "&".join([method, url.replace("http", "https"), data]) 87 | # print("&".join([method, url, data])) 88 | return "&".join([method, url, data]) 89 | 90 | class OrderEddsaSignHelper(EddsaSignHelper): 91 | def __init__(self, private_key): 92 | super(OrderEddsaSignHelper, self).__init__( 93 | poseidon_params(SNARK_SCALAR_FIELD, 12, 6, 53, b'poseidon', 5, security_target=128), 94 | private_key 95 | ) 96 | 97 | def serialize_data(self, order): 98 | return [ 99 | int(order["exchange"], 16), 100 | int(order["storageId"]), 101 | int(order["accountId"]), 102 | int(order["sellToken"]['tokenId']), 103 | int(order["buyToken"]['tokenId']), 104 | int(order["sellToken"]['volume']), 105 | int(order["buyToken"]['volume']), 106 | int(order["validUntil"]), 107 | int(order["maxFeeBips"]), 108 | int(order["fillAmountBOrS"]), 109 | int(order.get("taker", "0x0"), 16) 110 | ] 111 | 112 | class UpdateAccountEddsaSignHelper(EddsaSignHelper): 113 | def __init__(self, private_key): 114 | super(UpdateAccountEddsaSignHelper, self).__init__( 115 | poseidon_params(SNARK_SCALAR_FIELD, 9, 6, 53, b'poseidon', 5, security_target=128), 116 | private_key 117 | ) 118 | 119 | def serialize_data(self, updateAccount): 120 | return [ 121 | int(updateAccount['exchange'], 16), 122 | int(updateAccount['accountId']), 123 | int(updateAccount['maxFee']['tokenId']), 124 | int(updateAccount['maxFee']['volume']), 125 | int(updateAccount['publicKey']['x'], 16), 126 | int(updateAccount['publicKey']['y'], 16), 127 | int(updateAccount['validUntil']), 128 | int(updateAccount['nonce']) 129 | ] 130 | 131 | class OriginTransferEddsaSignHelper(EddsaSignHelper): 132 | def __init__(self, private_key): 133 | super(OriginTransferEddsaSignHelper, self).__init__( 134 | poseidon_params(SNARK_SCALAR_FIELD, 13, 6, 53, b'poseidon', 5, security_target=128), 135 | private_key 136 | ) 137 | 138 | def serialize_data(self, originTransfer): 139 | return [ 140 | int(originTransfer['exchange'], 16), 141 | int(originTransfer['payerId']), 142 | int(originTransfer['payeeId']), # payer_toAccountID 143 | int(originTransfer['token']['tokenId']), 144 | int(originTransfer['token']['volume']), 145 | int(originTransfer['maxFee']['tokenId']), 146 | int(originTransfer['maxFee']['volume']), 147 | int(originTransfer['payeeAddr'], 16), # payer_to 148 | 0, #int(originTransfer.get('dualAuthKeyX', '0'),16), 149 | 0, #int(originTransfer.get('dualAuthKeyY', '0'),16), 150 | int(originTransfer['validUntil']), 151 | int(originTransfer['storageId']) 152 | ] 153 | 154 | class DualAuthTransferEddsaSignHelper(EddsaSignHelper): 155 | def __init__(self, private_key): 156 | super(DualAuthTransferEddsaSignHelper, self).__init__( 157 | poseidon_params(SNARK_SCALAR_FIELD, 13, 6, 53, b'poseidon', 5, security_target=128), 158 | private_key 159 | ) 160 | 161 | def serialize_data(self, dualAuthTransfer): 162 | return [ 163 | int(dualAuthTransfer['exchange'],16), 164 | int(dualAuthTransfer['accountId']), 165 | int(dualAuthTransfer['payee_toAccountID']), 166 | int(dualAuthTransfer['token']), 167 | int(dualAuthTransfer['amount']), 168 | int(dualAuthTransfer['feeToken']), 169 | int(dualAuthTransfer['maxFeeAmount']), 170 | int(dualAuthTransfer['to'],16), 171 | int(dualAuthTransfer.get('dualAuthKeyX', '0'),16), 172 | int(dualAuthTransfer.get('dualAuthKeyY', '0'),16), 173 | int(dualAuthTransfer['validUntil']), 174 | int(dualAuthTransfer['storageId']), 175 | ] 176 | 177 | class WithdrawalEddsaSignHelper(EddsaSignHelper): 178 | def __init__(self, private_key): 179 | super(WithdrawalEddsaSignHelper, self).__init__( 180 | poseidon_params(SNARK_SCALAR_FIELD, 10, 6, 53, b'poseidon', 5, security_target=128), 181 | private_key 182 | ) 183 | 184 | def serialize_data(self, withdraw): 185 | return [ 186 | int(withdraw['exchange'], 16), 187 | int(withdraw['accountId']), 188 | int(withdraw['token']['tokenId']), 189 | int(withdraw['token']['volume']), 190 | int(withdraw['maxFee']['tokenId']), 191 | int(withdraw['maxFee']['volume']), 192 | int(withdraw['onChainDataHash'], 16), 193 | int(withdraw['validUntil']), 194 | int(withdraw['storageId']), 195 | ] 196 | 197 | class MessageHashEddsaSignHelper(EddsaSignHelper): 198 | def __init__(self, private_key): 199 | super(MessageHashEddsaSignHelper, self).__init__( 200 | poseidon_params(SNARK_SCALAR_FIELD, 2, 6, 53, b'poseidon', 5, security_target=128), 201 | private_key 202 | ) 203 | 204 | def hash(self, eip712_hash_bytes): 205 | return self.serialize_data(eip712_hash_bytes) 206 | 207 | def serialize_data(self, data): 208 | if isinstance(data, bytes): 209 | return int(data.hex(), 16) >> 3 210 | elif isinstance(data, str): 211 | return int(data, 16) >> 3 212 | else: 213 | raise TypeError("Unknown type " + str(type(data))) 214 | 215 | class MessageHash2EddsaSignHelper(EddsaSignHelper): 216 | def __init__(self, private_key): 217 | super(MessageHash2EddsaSignHelper, self).__init__( 218 | poseidon_params(SNARK_SCALAR_FIELD, 2, 6, 53, b'poseidon', 5, security_target=128), 219 | private_key 220 | ) 221 | 222 | def hash(self, eip712_hash_bytes): 223 | return self.serialize_data(eip712_hash_bytes) 224 | 225 | def serialize_data(self, data): 226 | if isinstance(data, bytes): 227 | return int(data.hex(), 16) % SNARK_SCALAR_FIELD 228 | elif isinstance(data, str): 229 | return int(data, 16) % SNARK_SCALAR_FIELD 230 | else: 231 | raise TypeError("Unknown type " + str(type(data))) 232 | -------------------------------------------------------------------------------- /sdk/sig_utils/hex_utils.py: -------------------------------------------------------------------------------- 1 | 2 | def prettyHex(input, size = 64): 3 | def hex2(v): 4 | if isinstance(input, int): 5 | s = hex(v)[2:] 6 | elif isinstance(input, str): 7 | if "0x" in input: 8 | s = v[2:] 9 | else: 10 | s = hex(int(input))[2:] 11 | elif isinstance(input, bytes): 12 | s = input.hex() 13 | else: 14 | raise Exception("Unknown input number type") 15 | return s if len(s) % 2 == 0 else '0' + s 16 | return "0x" + hex2(input).zfill(size) 17 | 18 | def prettyAddressHex(input): 19 | return prettyHex(input, size = 40) 20 | 21 | def prettyHashHex(input): 22 | return prettyHex(input, size = 64) 23 | 24 | def prettyEvenHex(input): 25 | return prettyHex(input, size = 0) -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/test/__init__.py -------------------------------------------------------------------------------- /test/exchange_utils/test_exchange.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | from web3 import Web3 4 | 5 | from sdk.eth_utils.exchange_utils import EthExchangeWrapper 6 | 7 | class TestApiSigns(unittest.TestCase): 8 | def setUp(self): 9 | self.exchange = EthExchangeWrapper( 10 | "your address", 11 | "your private key", 12 | "exchange address", 13 | "goerli", # chain name, conf can be found in exchange_utils 14 | "https://goerli.infura.io/v3/XXXXXXXXXXXX" # eth node provider 15 | ) 16 | print(f"setUpClass exchange done.") 17 | return super().setUp() 18 | 19 | def test_exchange_init(self): 20 | print(bytes.hex(self.exchange.getDomainSeparator())) 21 | print(self.exchange.getDepositContract()) 22 | 23 | def test_exchange_deposit_ETH(self): 24 | self.exchange.deposit("ETH", 0.01) 25 | 26 | def test_exchange_deposit_LRC(self): 27 | self.exchange.deposit("LRC", 1000) -------------------------------------------------------------------------------- /test/v3explorer/test_ecdsa.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from py_eth_sig_utils import utils as sig_utils 3 | from py_eth_sig_utils.signing import v_r_s_to_signature, signature_to_v_r_s 4 | from web3 import Web3 5 | 6 | from sdk.sig_utils.ecdsa_utils import * 7 | from time import time 8 | 9 | class TestApiSigns(unittest.TestCase): 10 | def setUp(self): 11 | "Hook method for setting up the test fixture before exercising it." 12 | EIP712.init_env(name="Loopring Protocol", 13 | version="3.6.0", 14 | chainId=5, 15 | verifyingContract="0x2FFfAa5D860B39b28467863a4454EE874127eF5E") 16 | 17 | EIP712.init_amm_env(name="pool-lrc-eth", 18 | version="1.0.0", 19 | chainId=1, 20 | verifyingContract="0x0000000000000000000000000000000000000001") 21 | 22 | pass 23 | 24 | def test_domain_separator(self): 25 | domainSep = EIP712.exchangeDomain.hash_struct() 26 | assert("0x" + bytes.hex(domainSep) == "0x0cf6ea7629bf5bc100a49a1508666d887795b6a97195eb205c6dafcd4fbe2328") 27 | 28 | def test_amm_domain_separator(self): 29 | EIP712.init_amm_env(name="AMM-YFI-ETH", 30 | version="1.0.0", 31 | chainId=1, 32 | verifyingContract="0xd85f594481D3DEE61FD38464dD54CF3ccE6906b6") 33 | domainSep = EIP712.ammPoolDomains['0xd85f594481D3DEE61FD38464dD54CF3ccE6906b6'].hash_struct() 34 | print("0x" + bytes.hex(domainSep).zfill(64)) 35 | 36 | def test_update_account_ecdsa_sig_uat(self): 37 | EIP712.init_env(name="Loopring Protocol", 38 | version="3.6.0", 39 | chainId=1337, 40 | verifyingContract="0x7489DE8c7C1Ee35101196ec650931D7bef9FdAD2") 41 | req = { 42 | 'exchange': '0x7489DE8c7C1Ee35101196ec650931D7bef9FdAD2', 43 | 'owner': "0x23a51c5f860527f971d0587d130c64536256040d", 44 | 'accountId': 10004, 45 | "publicKey" : { 46 | "x" : '0x2442c9e22d221abac0582cf764028d21114c9676b743f590741ffdf1f8a735ca', 47 | "y" : '0x08a42c954bc114b967bdd77cf7a1780e07fe10a4ebbef00b567ef2876e997d1a' 48 | }, 49 | "maxFee" : { 50 | "tokenId" : 0, 51 | "volume" : "4000000000000000" 52 | }, 53 | 'validUntil': 1700000000, 54 | 'nonce': 1 55 | } 56 | hash = generateUpdateAccountEIP712Hash(req) 57 | # print('createUpdateAccountMessage hash = 0x'+bytes.hex(hash)) 58 | assert('0x'+bytes.hex(hash) == "0x031fac4223887173ca741460e3b1e642d9d73371a64cd42b46212cc159877f03") 59 | 60 | def test_transfer_ecdsa_sig(self): 61 | EIP712.init_env(name="Loopring Protocol", 62 | version="3.6.0", 63 | chainId=1, 64 | verifyingContract="0x35990C74eB567B3bbEfD2Aa480467b1031b23eD9") 65 | req = { 66 | "exchange": "0x35990C74eB567B3bbEfD2Aa480467b1031b23eD9", 67 | "payerId": 0, 68 | "payerAddr": "0x611db73454c27e07281d2317aa088f9918321415", 69 | "payeeId": 0, 70 | "payeeAddr": "0xc0ff3f78529ab90f765406f7234ce0f2b1ed69ee", 71 | "token": { 72 | "tokenId": 0, 73 | "volume": str(1000000000000000000), 74 | }, 75 | "maxFee" : { 76 | "tokenId": 0, 77 | "volume": str(1000000000000000), 78 | }, 79 | "storageId": 1, 80 | "validUntil": 0xfffffff 81 | } 82 | hash = generateTransferEIP712Hash(req) 83 | print('generateTransferEIP712Hash hash = 0x'+bytes.hex(hash)) 84 | assert('0x'+bytes.hex(hash) == "0xcf3965e3eab3a47b1712b9cf8c7caa1af1a55a2e7a61869455ff64c6d9c791d1") 85 | v, r, s = sig_utils.ecsign(hash, int("1", 16).to_bytes(32, byteorder='big')) 86 | print(f"sig = {'0x' + bytes.hex(v_r_s_to_signature(v, r, s))}") 87 | # sig == v, r, s 88 | # assert(sig[0] == 27) 89 | # assert(sig[1] == 0x02d5ce917a2981c790f7e383f92c01735dd209d77d0b5ec422eec6207c5c486c) 90 | # assert(sig[2] == 0x72067b3c2501fb95674825933029d257f16f1955c200d3bb5162dfcc5dfd39d6) 91 | 92 | def test_withdraw_ecdsa_sig(self): 93 | EIP712.init_env(name="Loopring Protocol", 94 | version="3.6.0", 95 | chainId=1, 96 | verifyingContract="0x35990C74eB567B3bbEfD2Aa480467b1031b23eD9") 97 | extraData = bytes(0) 98 | onchainData = b''. join([int("0").to_bytes(32, 'big'), 99 | int("0x15127e64b546c5d0a9713d0b086e66a3359d8f6b", 16).to_bytes(20, 'big'), 100 | extraData]) 101 | onchainDataHash = Web3.keccak(onchainData)[:20] 102 | print(f"onchainData = {bytes.hex(onchainData)}, onchainDataHash = {'0x'+bytes.hex(onchainDataHash)}") 103 | req = { 104 | "exchange": "0x35990C74eB567B3bbEfD2Aa480467b1031b23eD9", 105 | "accountId": 5, 106 | "owner": "0x611db73454c27e07281d2317aa088f9918321415", 107 | "token": { 108 | "tokenId": 0, 109 | "volume": str(1000000000000000000), 110 | }, 111 | "maxFee" : { 112 | "tokenId": 0, 113 | "volume": str(1000000000000000), 114 | }, 115 | "to": "0xc0ff3f78529ab90f765406f7234ce0f2b1ed69ee", 116 | "onChainDataHash": "0x" + bytes.hex(onchainDataHash), 117 | "storageId": 5, 118 | "validUntil" : 0xfffffff, 119 | "minGas": 300000, 120 | "extraData": bytes.hex(extraData) 121 | } 122 | hash = generateOffchainWithdrawalEIP712Hash(req) 123 | print('generateOffchainWithdrawalEIP712Hash hash = 0x'+bytes.hex(hash)) 124 | assert('0x'+bytes.hex(hash) == "0xfae5a78e3d12d2c8b220ab8ae7bf733120285699c2c4441972986044c02cbb06") 125 | v, r, s = sig_utils.ecsign(hash, int("1", 16).to_bytes(32, byteorder='big')) 126 | print(f"sig = {'0x' + bytes.hex(v_r_s_to_signature(v, r, s))}") 127 | 128 | def test_amm_join_ecdsa_sig_1(self): 129 | EIP712.init_amm_env(name="AMM Pool LRC-ETH", 130 | version="1.0.0", 131 | chainId=5, 132 | verifyingContract="0xDadF20fc684C11ce9a8713C7fdd496865562764f") 133 | req = { 134 | 'poolAddress': "0xd95EE4302E49963CB751945c48BD553fd093", 135 | 'owner': "0x23a51c5f860527f971d0587d130c64536256040d", 136 | "joinTokens" : { 137 | "pooled" : [ 138 | { 139 | "tokenId": 1, 140 | "volume" : "100000000000000000000" 141 | }, 142 | { 143 | "tokenId": 0, 144 | "volume" : "100000000000000000000" 145 | }, 146 | ], 147 | "minimumLp" : { 148 | "tokenId" : 5, 149 | "volume" : "100000000000" 150 | } 151 | }, 152 | 'storageIds': [1,1], 153 | 'validUntil': 1700000000 154 | } 155 | hash = generateAmmPoolJoinEIP712Hash(req) 156 | print('generateAmmPoolJoinEIP712Hash hash = 0x'+bytes.hex(hash)) 157 | # assert('0x'+bytes.hex(hash) == "0xbfda6876a7fedf9f6403000d306f41bcc5e8c10330aedb99d4503f866efbc895") 158 | v, r, s = sig_utils.ecsign(hash, int("0x4c5496d2745fe9cc2e0aa3e1aad2b66cc792a716decf707ddb3f92bd2d93ad24", 16).to_bytes(32, byteorder='big')) 159 | print(f"sig = {'0x' + bytes.hex(v_r_s_to_signature(v, r, s))}") 160 | 161 | def test_amm_exit_ecdsa_sig(self): 162 | EIP712.init_amm_env(name="pool-lrc-eth", 163 | version="1.0.0", 164 | chainId=1, 165 | verifyingContract="0xd95EE4302E49963CB751945c48BD553fd093") 166 | req = { 167 | 'poolAddress': "0xd95EE4302E49963CB751945c48BD553fd093", 168 | 'owner': "0x23a51c5f860527f971d0587d130c64536256040d", 169 | "exitTokens" : { 170 | "unPooled" : [ 171 | { 172 | "tokenId": 1, 173 | "volume" : "100000000000000000000" 174 | }, 175 | { 176 | "tokenId": 0, 177 | "volume" : "100000000000000000000" 178 | }, 179 | ], 180 | "burned" : { 181 | "tokenId" : 5, 182 | "volume" : "100000000000" 183 | } 184 | }, 185 | 'storageId': 1, 186 | 'maxFee': "1000000000", 187 | 'validUntil': 1700000000 188 | } 189 | hash = generateAmmPoolExitEIP712Hash(req) 190 | print('generateAmmPoolExitEIP712Hash hash = 0x'+bytes.hex(hash)) 191 | assert('0x'+bytes.hex(hash) == "0x3a4fbd83181adf60cfdc176e25eddc761e069553eb72de7022d3165b43b08dd4") 192 | v, r, s = sig_utils.ecsign(hash, int("1", 16).to_bytes(32, byteorder='big')) 193 | print(f"sig = {'0x' + bytes.hex(v_r_s_to_signature(v, r, s))}") 194 | 195 | def test_hw_wallet_ecdsa_sign_1(self): 196 | ecdsaSig = "0xf3fde24c40614bd48d22ab005bb9b57215ff77d390744aa145cbb6a7004532584bd4ecdde4cf1b5df5ed0e518b01cfad79a84cd50a457f524ff42b50c1b715691b" 197 | owner = "0x6b1029c9ae8aa5eea9e045e8ba3c93d380d5bdda" 198 | v, r, s = signature_to_v_r_s(bytes.fromhex(ecdsaSig.replace("0x", ""))) 199 | origin_message = "0xc53367f8714c5cbb86b39de72f9010a11564948dc72f7dfad6f9007efae98802" 200 | origin_message_bytes = bytes.fromhex(origin_message.replace("0x", "")) 201 | # message_toSign = Web3.keccak(b"\x19Ethereum Signed Message:\n" + bytes(f"{len(origin_message_bytes)}", 'utf8') + origin_message_bytes) 202 | k = sig_utils.ecrecover_to_pub(origin_message_bytes, v, r, s) 203 | address = Web3.keccak(k)[-20:].hex() 204 | assert(address.lower() == owner) 205 | 206 | def test_hw_wallet_ecdsa_sign_2(self): 207 | ecdsaSig = "0xb726f15413de8be5756a50b261c92a9689c5e45301ceafc2f4498984a57b51f27355ae7084c1701668ac44dbebca9158b8c92a16eb7dd6fe9b98d72896b54fce1b" 208 | owner = "0x2ca1ac470909bee7f83b432348787ca8df800131" 209 | v, r, s = signature_to_v_r_s(bytes.fromhex(ecdsaSig.replace("0x", ""))) 210 | origin_message = "1" 211 | origin_message_bytes = bytes(origin_message, 'utf-8') 212 | message_toSign = Web3.keccak(b"\x19Ethereum Signed Message:\n" + bytes(f"{len(origin_message_bytes)}", 'utf8') + origin_message_bytes) 213 | k = sig_utils.ecrecover_to_pub(message_toSign, v, r, s) 214 | address = Web3.keccak(k)[-20:].hex() 215 | assert(address.lower() == owner) 216 | 217 | if __name__ == '__main__': 218 | unittest.main() 219 | -------------------------------------------------------------------------------- /test/v3explorer/test_eddsa.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest 3 | from web3 import Web3 4 | 5 | from sdk.loopring_v3_client import LOOPRING_REST_HOST 6 | from sdk.sig_utils.eddsa_utils import * 7 | from sdk.ethsnarks.jubjub import Point 8 | from sdk.ethsnarks.eddsa import Signature, SignedMessage 9 | from time import time 10 | from sdk.request_utils.rest_client import Request 11 | 12 | class TestEddsaSignHelpers(unittest.TestCase): 13 | 14 | @classmethod 15 | def setUpClass(cls): 16 | privKey, publicKey = PoseidonEdDSA.random_keypair() 17 | cls.privKey = hex(int(privKey)) 18 | cls.publicKey = publicKey 19 | print(f"setUpClass: priv = {cls.privKey}") 20 | 21 | def test_order_signer_helper(self): 22 | signer = DummyEddsaSignHelper(private_key = self.privKey) 23 | data = {"data": 5} 24 | msg = signer.hash(data) 25 | sig = signer.sign(data) 26 | signature = signer.sigStrToSignature(sig) 27 | generatedSig = SignedMessage(Point(self.publicKey.x, self.publicKey.y), signature, msg) 28 | assert signer.verify(data, generatedSig) 29 | 30 | def test_url_signer(self): 31 | signer = UrlEddsaSignHelper("0x1234", LOOPRING_REST_HOST) 32 | url = urllib 33 | request = Request( 34 | method="GET", 35 | path="/api/v3/apiKey", 36 | params={"accountId": 10001}, 37 | data={}, 38 | headers={} 39 | ) 40 | hash = signer.hash(request) 41 | print(f"hash = {hex(hash)}") 42 | 43 | signedMessage = signer.sign(request) 44 | print(f"signedMessage = {signedMessage}") 45 | 46 | signature = signer.sigStrToSignature("0x0dfbbcc409fbcdb07dc350b50cf034fa0ebfd259346c81b3fd3bdb8951117a152ccecf37615e470d0038f15fd1c1ea69f212636033d880119b474e1e9e7548131074a9dad709b4e4950a86fea510d9c0b207a66f35aab4fc135e5fe64b9d009b") 47 | signedMessage = SignedMessage( 48 | PoseidonEdDSA.B() * 0x1234, 49 | signature, 50 | hash 51 | ) 52 | assert signer.verify(signedMessage.msg, signedMessage) 53 | 54 | def test_updateAccount_eddsa_hash(self): 55 | req = { 56 | "exchange": "0x80eB675C448602284a3e2090d873aF2af0450Fd9", 57 | 'owner': "0x3fce664c0a76b1f91ae9ad767545bec70875bf71", 58 | 'accountId': 10001, 59 | "publicKey" : { 60 | "x" : "0x2ea2c249f7ffa335363ecbfbfe07a73268e5565ecaa9468e1140410f1349e16a", 61 | "y" : "0x10da55ef2a7235bb192d47554f4d3d5091d6854b686471270d5c3838b3ed733c", 62 | }, 63 | "maxFee" : { 64 | "tokenId" : 0, 65 | "volume" : "4000000000000000" 66 | }, 67 | 'validUntil': 1700000000, 68 | 'nonce': 2 69 | } 70 | signer = UpdateAccountEddsaSignHelper("0x623167b48a61c02c546fef1bb0d810f4c0d14802b7669ef5b9de9af83212de") 71 | hash = signer.hash(req) 72 | print(f"hash = {hex(hash)}") 73 | 74 | signedMessage = signer.sign(req) 75 | print(f"signedMessage = {signedMessage}") 76 | 77 | signature = signer.sigStrToSignature(signedMessage) 78 | signedMessage = SignedMessage( 79 | PoseidonEdDSA.B() * 0x623167b48a61c02c546fef1bb0d810f4c0d14802b7669ef5b9de9af83212de, 80 | signature, 81 | hash 82 | ) 83 | assert signer.verify(signedMessage.msg, signedMessage) 84 | 85 | def test_withdraw_eddsa_sig(self): 86 | extraData = bytes(0) 87 | onchainData = b''. join([int("0").to_bytes(32, 'big'), 88 | int("0x23a51c5f860527f971d0587d130c64536256040d", 16).to_bytes(20, 'big'), 89 | extraData]) 90 | onchainDataHash = Web3.keccak(onchainData)[:20] 91 | assert('0x' + bytes.hex(onchainDataHash) == "0x09b0a56ec6c45c6f3af2abbdefd66b6e84bce8e4") 92 | 93 | # print(f"onchainData = {bytes.hex(onchainData)}, onchainDataHash = {'0x'+bytes.hex(onchainDataHash)}") 94 | req = { 95 | "exchange": "0x35990C74eB567B3bbEfD2Aa480467b1031b23eD9", 96 | "accountId": 5, 97 | "owner": "0x23a51c5f860527f971d0587d130c64536256040d", 98 | "token": { 99 | "tokenId": 0, 100 | "volume": str(1000000000000000000), 101 | }, 102 | "maxFee" : { 103 | "tokenId": 0, 104 | "volume": str(1000000000000000), 105 | }, 106 | "to": "0xc0ff3f78529ab90f765406f7234ce0f2b1ed69ee", 107 | "onChainDataHash": "0x" + bytes.hex(onchainDataHash), 108 | "storageId": 5, 109 | "validUntil" : 0xfffffff, 110 | "minGas": 300000, 111 | "extraData": bytes.hex(extraData) 112 | } 113 | signer = WithdrawalEddsaSignHelper("0x4a3d1e098350") 114 | hash = signer.hash(req) 115 | assert(hex(hash) == "0x1c59c63f9bf24d97195d64d828af72e2037a4022413804e410799682d960f09c") 116 | 117 | def test_order_eddsa_sig(self): 118 | order = { 119 | # sign part 120 | "exchange" : "0x7489DE8c7C1Ee35101196ec650931D7bef9FdAD2", 121 | "accountId" : 10004, 122 | "storageId" : 0, 123 | "sellToken": { 124 | "tokenId": 0, 125 | "volume": "90000000000000000000" 126 | }, 127 | "buyToken" : { 128 | "tokenId": 1, 129 | "volume": "100000000000000000000" 130 | }, 131 | "validUntil" : 1700000000, 132 | "maxFeeBips" : 50, 133 | "fillAmountBOrS": True, 134 | # "taker" : "0000000000000000000000000000000000000000", 135 | # aux data 136 | "allOrNone" : False, 137 | "clientOrderId" : "SampleOrder-" + str(int(time()*1000)), 138 | "orderType" : "LIMIT_ORDER" 139 | } 140 | 141 | signer = OrderEddsaSignHelper(hex(56869496543825)) 142 | msgHash = signer.hash(order) 143 | signedMessage = signer.sign(order) 144 | print(f"msgHash = {hex(msgHash)}") 145 | # print(f"signedMessage = {signedMessage}") 146 | 147 | def test_ocdh(self): 148 | onchainDataHash = Web3.keccak(b''. join([int("0").to_bytes(32, 'big'), 149 | int("0x23A51c5f860527F971d0587d130c64536256040D", 16).to_bytes(20, 'big'), 150 | bytes(0)])) 151 | print(bytes.hex(onchainDataHash[:20])) 152 | assert(bytes.hex(onchainDataHash[:20]) == "09b0a56ec6c45c6f3af2abbdefd66b6e84bce8e4") 153 | 154 | def test_hash_signer(self): 155 | key = "0x5" 156 | signer = MessageHashEddsaSignHelper(key) 157 | msg = "0xABCD1234" 158 | hash = signer.hash(msg) 159 | assert hash == 360292934 160 | 161 | sigStr = ("0x02c8188eae892aab8156d117fb3b00cea67cdcf2e7c58eea75ac422bd0921417095bfe981854b2c56972f2343e717b293731de7d43984dacb9e91e9767f0581b1de6fbe479d71c7ee016595c0ad62c9fc7b31b52fd7dcbcc259c86ac2f04f09d") 162 | signature = signer.sigStrToSignature(sigStr) 163 | signedMessage = SignedMessage( 164 | PoseidonEdDSA.B() * int(key, 16), 165 | signature, 166 | 360292934 167 | ) 168 | assert signer.verify(signedMessage.msg, signedMessage) 169 | 170 | if __name__ == "__main__": 171 | unittest.main() 172 | -------------------------------------------------------------------------------- /tutorials/README.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | Loopring V3 api tutorials 4 | 5 | ## Directory Contents 6 | 7 | ```text 8 | . 9 | ├── hash_and_sign : See how poseidon hash and eddsa signing works 10 | └── transfer: A complete flow to make a transfer in L2 Loopring DEX 11 | ``` 12 | -------------------------------------------------------------------------------- /tutorials/hash_and_sign/README.md: -------------------------------------------------------------------------------- 1 | # Hash and Signature Tutorial 2 | 3 | ## hash example command 4 | $python poseidon_hash_sample.py -a hash -i 1,2,3 5 | poseidon_hash [1, 2, 3] 6 | hash of [1, 2, 3] is 20693456676802104653139582814194312788878632719314804297029697306071204881418 7 | 8 | ## Sign example command 9 | $python poseidon_hash_sample.py -a sign -i 1,2,3 -k 0x1 10 | loopring sign message [1, 2, 3] 11 | signature of '[1, 2, 3]' is 0x0b09268fe04061cdee982d1b7c99a99792409064e18f79ee7068ad66789a4c7000efa194cea14cbac78611bebafee72f08d18e8ee7f3dbdfe3a086d8729614ad0a57beb30e2d905618e2e4b73f9eec261957bf00c5c7788ecd1e0c3ec4fbaea9 12 | -------------------------------------------------------------------------------- /tutorials/hash_and_sign/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/tutorials/hash_and_sign/__init__.py -------------------------------------------------------------------------------- /tutorials/hash_and_sign/poseidon_hash_sample.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import sys 3 | 4 | from sdk.ethsnarks.eddsa import PureEdDSA, PoseidonEdDSA 5 | from sdk.ethsnarks.field import FQ, SNARK_SCALAR_FIELD 6 | from sdk.ethsnarks.poseidon import poseidon_params, poseidon 7 | from sdk.sig_utils.eddsa_utils import * 8 | import argparse 9 | 10 | MAX_INPUT = 13 11 | 12 | class TutorialEddsaSignHelper(EddsaSignHelper): 13 | def __init__(self, private_key="0x1"): 14 | super(TutorialEddsaSignHelper, self).__init__( 15 | poseidon_params = poseidon_params(SNARK_SCALAR_FIELD, MAX_INPUT + 1, 6, 53, b'poseidon', 5, security_target=128), 16 | private_key = private_key 17 | ) 18 | 19 | def serialize_data(self, inputs): 20 | return [int(data) for data in inputs][:MAX_INPUT] 21 | 22 | def loopring_poseidon_hash(inputs): 23 | # prepare params, using loopring order params 24 | print(f"poseidon_hash {inputs}") 25 | hasher = TutorialEddsaSignHelper() 26 | hash_value = hasher.hash(inputs) 27 | return hash_value 28 | 29 | def loopring_sign(input_message, private_key): 30 | print(f"loopring sign message {input_message}") 31 | hasher = TutorialEddsaSignHelper(private_key) 32 | signature = hasher.sign(inputs) 33 | return signature 34 | 35 | if __name__ == "__main__": 36 | parser = argparse.ArgumentParser(description="Loopring Hash and Sign Code Sample") 37 | parser.add_argument("-a", "--action", required=True, choices=['hash', 'sign'], help='choose action, "hash" calculates poseidon hash of inputs. "sign" signs the message.') 38 | parser.add_argument("-i", "--inputs", help='hash or sign message inputs. For poseidon hash, they should be number string list separated by "," like “1,2,3,4,5,6”, max len is 13 to compatible with loopring DEX config') 39 | parser.add_argument("-k", "--privatekey", default=None, help='private key to sign the inputs, should be a big int string, like “12345678”, user can try the key exported from loopring DEX') 40 | 41 | args = parser.parse_args() 42 | 43 | if args.action == "sign": 44 | inputs = [int(i) for i in args.inputs.split(',')] 45 | private_key = args.privatekey 46 | assert private_key is not None and private_key[:2] == '0x' 47 | sign = loopring_sign(inputs, private_key) 48 | print(f"signature of '{inputs}' is {sign}") 49 | elif args.action == "hash": 50 | inputs = [int(i) for i in args.inputs.split(',')] 51 | assert len(inputs) <= MAX_INPUT 52 | hash_value = loopring_poseidon_hash(inputs) 53 | print(f"hash of {inputs} is {hash_value}") 54 | -------------------------------------------------------------------------------- /tutorials/transfer/README.md: -------------------------------------------------------------------------------- 1 | # Transfer Tutorial 2 | 3 | ## Getting started 4 | 5 | In this tutorial we will demonstrate how to: 6 | 7 | 1. Connect to the loopring DEX. 8 | 2. Query assets in loopring DEX. 9 | 3. Make transfers. 10 | 4. Query transfer records. 11 | 12 | ## Export the loopring DEX Account 13 | 14 | 1. Export account info from Loopring DEX, take an account in [Loopring UAT env](beta.loopring.io) as an example. Also user may need eth private key to sign extra authentication to show the ownership of the L1 address. Professional users can register to a whitelist in Loopring to skip that check. 15 | 16 | ```python 17 | export_account = { 18 | "chainId": 5, # UAT in goerli, chainID is 5. ETH mainnet is 1. 19 | "exchangeName": "Loopring Exchange v2", 20 | "exchangeAddress": "0x2e76EBd1c7c0C8e7c2B875b6d505a260C525d25e", 21 | "accountAddress": "0x2cA82103BF777bF0e7a73B9577Ce26CfFD9bec6f", 22 | "accountId": 10000, 23 | "apiKey": "JPxqxPF992s7KzN7SLSzhyccb3BAUY3HOtQnDylXWMiASnJADr4awM2MV2bWQGR0", 24 | "publicKeyX": "0x029ee1d99d7ee60047698db065f4845dbba642fcfd95a72ad267d467f4d37d54", 25 | "publicKeyY": "0xb777dcadaee66fadd6cef8c60107df88ace06bc5ab7ce3fb69206cf5d8b006a1", 26 | "privateKey": "0x4f7cb9a17e1d375947db4a0117a556047d81732cdb4b6ef0bff167c20bb1ee9" 27 | } 28 | 29 | eth_private_key = "0x0" # EOA private key to sign L1 authentication. 30 | whitelisted = False 31 | ``` 32 | 33 | ## Query assets in loopring DEX 34 | 35 | 1. Make query request 36 | 37 | ```python 38 | param = { 39 | "accountId": export_account['accountId'], 40 | "tokens": '0,1,2' 41 | } 42 | ``` 43 | 44 | 2. Get query response 45 | 46 | ```python 47 | import requests 48 | 49 | response = requests.request( 50 | method="GET", 51 | url="https://uat2.loopring.io/api/v3/user/balances", 52 | headers={"X-API-KEY": export_account['apiKey']}, 53 | params=param 54 | ) 55 | ``` 56 | 57 | ​ The response should be like: 58 | 59 | ```python 60 | [ 61 | { 62 | "tokenId": 0, 63 | "total": "1000000000000000000", 64 | "locked": "0", 65 | "pending": { 66 | "deposit": "0", 67 | "withdraw": "0" 68 | } 69 | }, 70 | { 71 | "tokenId": 1, 72 | "total": "200000000000000000000", 73 | "locked": "0", 74 | "pending": { 75 | "deposit": "0", 76 | "withdraw": "0" 77 | } 78 | } 79 | ] 80 | ``` 81 | 82 | ## Make transfers 83 | 84 | 1. Make a transfer request 85 | 86 | Before making a transfer, the user needs to get storageId of the token to be transferred, which is just like the nonce in ETH tx. 87 | 88 | ```python 89 | import requests 90 | 91 | response = requests.request( 92 | method="GET", 93 | url="https://uat2.loopring.io/api/v3/storageId", 94 | headers={"X-API-KEY": export_account['apiKey']}, 95 | params={ 96 | "accountId": export_account['accountId'], 97 | "sellTokenId": 0 98 | } 99 | ) 100 | ``` 101 | 102 | Response is: 103 | 104 | ```python 105 | { 106 | "offchainId": 1001, 107 | "orderId": 0 108 | } 109 | ``` 110 | 111 | Then, user needs to query transfer fee requirement 112 | 113 | ```python 114 | import requests 115 | 116 | response = requests.request( 117 | method="GET", 118 | url="https://uat2.loopring.io/api/v3/user/offchainFee", 119 | headers={"X-API-KEY": export_account['apiKey']}, 120 | params={ 121 | "accountId": export_account['accountId'], 122 | "requestType": 3, 123 | "tokenSymbol": "ETH" 124 | } 125 | ) 126 | ``` 127 | 128 | Response is like: 129 | 130 | ```python 131 | { 132 | "fees": [ 133 | { 134 | "discount": 1, 135 | "fee": "26200000000000", 136 | "token": "ETH" 137 | }, 138 | { 139 | "discount": 0.8, 140 | "fee": "165800000000000000", 141 | "token": "LRC" 142 | }, 143 | { 144 | "discount": 0.8, 145 | "fee": "51800", 146 | "token": "USDT" 147 | }, 148 | { 149 | "discount": 1, 150 | "fee": "647000000000000000", 151 | "token": "DAI" 152 | } 153 | ], 154 | "gasPrice": "30000000000" 155 | } 156 | ``` 157 | 158 | Time to create transfer requirement: 159 | 160 | ```python 161 | transferReq = { 162 | "exchange": export_account["exchangeAddress"], 163 | "payerId": export_account["accountId"], 164 | "payerAddr": export_account["accountAddress"], 165 | "payeeId": 0, # if you don't know the dest id, leave it to 0. 166 | "payeeAddr": "0xAA520c7F9674aAB15e6d3A98A72A93eAe6E751b0", # trans to a specific L2 account 167 | "token": { 168 | "tokenId": 0, 169 | "volume": "1000000000000000000" 170 | }, 171 | "maxFee" : { 172 | "tokenId": 0, 173 | "volume": feeAmount 174 | }, 175 | "storageId": storageId, 176 | "validUntil": 1700000000, 177 | "memo": f"tutorial test {storageId}" 178 | } 179 | ``` 180 | 181 | 2. Sign transfer request 182 | 183 | ```python 184 | from sdk.sig_utils.eddsa_utils import OriginTransferEddsaSignHelper 185 | 186 | signer = OriginTransferEddsaSignHelper(export_account["privateKey"]) 187 | signedMessage = signer.sign(transferReq) 188 | transferReq.update({{"eddsaSignature": signedMessage}}) 189 | ``` 190 | 191 | Common users need extra L1 ecdsa authentication in request header to show their ownership of the address, so an extra signature based on L1 EIP712 is required. But this is not necessary for those professional users registered to Loopring. 192 | 193 | ```python 194 | headers={ 195 | "X-API-KEY": export_account["apiKey"] 196 | } 197 | if ecdsaAuth: 198 | message = generateTransferEIP712Hash(transferReq) 199 | # print(f"transfer message hash = {bytes.hex(message)}") 200 | v, r, s = sig_utils.ecsign(message, ecdsaKey) 201 | # will put into header, need L1 sig verification 202 | headers.update({'X-API-SIG': "0x" + bytes.hex(v_r_s_to_signature(v, r, s)) + EthSignType.EIP_712}) 203 | ``` 204 | 205 | 3. Send to Loopring and wait for response 206 | 207 | ```python 208 | import requests 209 | 210 | response = requests.request( 211 | method="POST", 212 | url="https://uat2.loopring.io/api/v3/transfer", 213 | headers=headers, 214 | json=transferReq 215 | ``` 216 | 217 | Then the response should be: 218 | 219 | ```python 220 | { 221 | 'hash': '0x2a38b31988359f742c65b668853adaf82bade728f1faf8f0748283f853e83b59', 222 | 'status': 'processing', 223 | 'isIdempotent': False 224 | } 225 | ``` 226 | 227 | 4. Check transfer record 228 | 229 | ```python 230 | response = requests.request( 231 | method="GET", 232 | url="https://uat2.loopring.io/api/v3/user/transfers", 233 | headers={"X-API-KEY": export_account["apiKey"]}, 234 | params = { 235 | "accountId": export_account["accountId"] 236 | } 237 | ) 238 | ``` 239 | 240 | The records is as below: 241 | 242 | ```python 243 | { 244 | "totalNum": 2, 245 | "transactions": [ 246 | { 247 | "amount": "100000000000000000000", 248 | "feeAmount": "26200000000000", 249 | "feeTokenSymbol": "ETH", 250 | "hash": "0x020fbe9eee00c1fd9dc1356b5cd01949ce844f1a65428b295b00559fe08dbf3f", 251 | "id": 383387, 252 | "memo": "test 1003 token(1) transfer from hello_loopring", 253 | "progress": "100%", 254 | "receiver": 10032, 255 | "receiverAddress": "0xaa520c7f9674aab15e6d3a98a72a93eae6e751b0", 256 | "senderAddress": "0x2cbfa87779572103ffd97cbf0e26ce7a73bbec6f", 257 | "status": "processed", 258 | "symbol": "LRC", 259 | "timestamp": 1623941490045, 260 | "txType": "TRANSFER", 261 | "updatedAt": 1623941490171 262 | }, 263 | { 264 | "amount": "100000000000000000000", 265 | "feeAmount": "26200000000000", 266 | "feeTokenSymbol": "ETH", 267 | "hash": "0x2a38b31988359f742c65b668853adaf82bade728f1faf8f0748283f853e83b59", 268 | "id": 383384, 269 | "memo": "test 1001 token(1) transfer from hello_loopring", 270 | "progress": "100%", 271 | "receiver": 10032, 272 | "receiverAddress": "0xaa520c7f9674aab15e6d3a98a72a93eae6e751b0", 273 | "senderAddress": "0x2cbfa87779572103ffd97cbf0e26ce7a73bbec6f", 274 | "status": "processed", 275 | "symbol": "LRC", 276 | "timestamp": 1623941473913, 277 | "txType": "TRANSFER", 278 | "updatedAt": 1623941474341 279 | } 280 | ] 281 | } 282 | ``` 283 | 284 | ​ The`"status": "processed"`indicates the transfer `"0x2a38b31988359f742c65b668853adaf82bade728f1faf8f0748283f853e83b59"` is done. 285 | 286 | -------------------------------------------------------------------------------- /tutorials/transfer/transfer_tutorial.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from py_eth_sig_utils import utils as sig_utils 4 | from py_eth_sig_utils.signing import v_r_s_to_signature 5 | import time 6 | 7 | from sdk.sig_utils.eddsa_utils import OriginTransferEddsaSignHelper 8 | from sdk.sig_utils.ecdsa_utils import EIP712, generateTransferEIP712Hash 9 | from sdk.loopring_v3_client import EthSignType 10 | 11 | whitelisted = False # Set to False if you are the whitelisted professional user. 12 | eth_private_key = "0x1" # EOA only 13 | 14 | # use exported UAT account from beta.loopring.io 15 | export_account = { 16 | "name" : "DEV Account 1", 17 | "exchangeName": "LoopringDEX: V2", 18 | "chainId": 5, # UAT in goerli, chainID is 5 19 | "exchangeAddress": "0x2e76EBd1c7c0C8e7c2B875b6d505a260C525d25e", 20 | "accountAddress": "", 21 | "accountId": 0, 22 | "apiKey": "", 23 | "publicKeyX": "0x0", 24 | "publicKeyY": "0x0", 25 | "privateKey": "0x0" 26 | } 27 | 28 | print("1. Query user balance") 29 | response = requests.request( 30 | method="GET", 31 | url="https://uat2.loopring.io/api/v3/user/balances", 32 | headers={"X-API-KEY": export_account['apiKey']}, 33 | params={ 34 | "accountId": export_account['accountId'], 35 | "tokens": '0' 36 | } 37 | ) 38 | print("user balance:") 39 | print(json.dumps(response.json(), indent=4, sort_keys=False)) 40 | 41 | print("2. Query token storageId") 42 | response = requests.request( 43 | method="GET", 44 | url="https://uat2.loopring.io/api/v3/storageId", 45 | headers={"X-API-KEY": export_account['apiKey']}, 46 | params={ 47 | "accountId": export_account['accountId'], 48 | "sellTokenId": 0 49 | } 50 | ) 51 | print("token 0 storageId:") 52 | print(json.dumps(response.json(), indent=4, sort_keys=False)) 53 | storageId = response.json()["offchainId"] 54 | 55 | print("3. Query transfer fee") 56 | response = requests.request( 57 | method="GET", 58 | url="https://uat2.loopring.io/api/v3/user/offchainFee", 59 | headers={"X-API-KEY": export_account['apiKey']}, 60 | params={ 61 | "accountId": export_account['accountId'], 62 | "requestType": 3, 63 | "tokenSymbol": "ETH" 64 | } 65 | ) 66 | print("token ETH transfer fee:") 67 | print(json.dumps(response.json(), indent=4, sort_keys=False)) 68 | feeToken = 0 #response.json()["fees"][0]["token"] 69 | feeAmount = response.json()["fees"][0]["fee"] 70 | 71 | print("4. Request transfer") 72 | transferReq = { 73 | "exchange": export_account["exchangeAddress"], 74 | "payerId": export_account["accountId"], 75 | "payerAddr": export_account["accountAddress"], 76 | "payeeId": 0, # if you don't know the dest id, leave it to 0. 77 | "payeeAddr": "0xAA520c7F9674aAB15e6d3A98A72A93eAe6E751b0", 78 | "token": { 79 | "tokenId": 0, 80 | "volume": "1000000000000000000" 81 | }, 82 | "maxFee" : { 83 | "tokenId": 0, 84 | "volume": feeAmount 85 | }, 86 | "storageId": storageId, 87 | "validUntil": 1700000000, 88 | "memo": f"tutorial test {storageId}" 89 | } 90 | 91 | signer = OriginTransferEddsaSignHelper(export_account["privateKey"]) 92 | signedMessage = signer.sign(transferReq) 93 | transferReq.update({"eddsaSignature": signedMessage}) 94 | 95 | headers={ 96 | "X-API-KEY": export_account["apiKey"] 97 | } 98 | if not whitelisted: 99 | EIP712.init_env(name="Loopring Protocol", 100 | version="3.6.0", 101 | chainId=export_account['chainId'], 102 | verifyingContract=export_account['exchangeAddress']) 103 | message = generateTransferEIP712Hash(transferReq) 104 | # print(f"transfer message hash = {bytes.hex(message)}") 105 | ethPrivKey = int(eth_private_key, 16).to_bytes(32, byteorder='big') 106 | v, r, s = sig_utils.ecsign(message, ethPrivKey) 107 | # will put into header, need L1 sig verification 108 | headers.update({'X-API-SIG': "0x" + bytes.hex(v_r_s_to_signature(v, r, s)) + EthSignType.EIP_712}) 109 | 110 | response = requests.request( 111 | method="POST", 112 | url="https://uat2.loopring.io/api/v3/transfer", 113 | headers=headers, 114 | json=transferReq 115 | ) 116 | print(json.dumps(response.json(), indent=4, sort_keys=False)) 117 | 118 | print("5. Query last transfer status") 119 | response = requests.request( 120 | method="GET", 121 | url="https://uat2.loopring.io/api/v3/user/transfers", 122 | headers={"X-API-KEY": export_account["apiKey"]}, 123 | params = { 124 | "accountId": export_account["accountId"], 125 | "start": int(time.time()*1000 - 3 * 1000), 126 | "tokenSymbol": "ETH" 127 | } 128 | ) 129 | print(json.dumps(response.json(), indent=4, sort_keys=False)) 130 | -------------------------------------------------------------------------------- /v3explorer/README.md: -------------------------------------------------------------------------------- 1 | # Loopring V3 api sample. 2 | 3 | ## install 4 | 5 | ```bash 6 | $pip install -r requirements.txt # install lib dependencies 7 | $export PYTHONPATH=${PWD} # use local ethsnarks 8 | ``` 9 | 10 | ## command line usage 11 | 12 | ### setup account 13 | Set config got from [Loopring UAT Env](https://loopring-amm.herokuapp.com/), the exported value is like this: 14 | ```python 15 | loopring_exported_account = { 16 | "name" : "DEV Account 1", 17 | "exchangeName": "LoopringDEX: V2", 18 | "exchangeAddress": "0x2e76EBd1c7c0C8e7c2B875b6d505a260C525d25e", 19 | "chainId": 5, 20 | "accountAddress": "0x2c87779572103fFD97CbF0BFAe26Ce7a73Bbec6f", 21 | "accountId": 10000, 22 | "apiKey": "J7SXWMiASnJADr4awM2SzhycLPxqxPF992nDylMs7KzNcb3BAUY3HOtQV2bWQGR0", 23 | "publicKeyX": "0x02e600476845fcfd95a72ad267d469db98db065f4ba642ee1d99d7e7f4d37d54", 24 | "publicKeyY": "0x06bc5ab7c06b777dcadaee66fadd6cef8c6010e3fb6927df88acecf5d8b006a1", 25 | "privateKey": "0x4f047d81732cdb4b6ef00117a57cb9bff167c20bb17e1d375947db4aa561ee9", 26 | # extra settings 27 | "ecdsaKey" : "0x1", 28 | "whitelisted": False 29 | } 30 | ``` 31 | There are 2 extra settings. ecdsaKey is for L1 account ownership authentication, directly moving L2 token operations like transfer & withdraw, need extra L1 signatures. However, if you are a professional user, you can register to Loopring's whitelist to skip this step, which makes transfers, especially payments, convenient & fast. 32 | 33 | ### update passowrd 34 | 35 | ```bash 36 | $python v3explorer/api_explorer.py -a update -k 0x4f047d81732cdb4b6ef00117a57cb9bff167c20bb17e1d375947db4aa561ee9 37 | ``` 38 | 39 | ### transfer 40 | 41 | ```bash 42 | $python v3explorer/api_explorer.py -a transfer -t LRC -v 100 -u 0xd854872f17c2783ae9d89e7b2a29cd72ec2a74ff 43 | ``` 44 | 45 | ### withdraw 46 | 47 | ```bash 48 | $python v3explorer/api_explorer.py -a withdraw -t LRC -v 5000 49 | ``` 50 | 51 | ### order 52 | 53 | ```bash 54 | $python v3explorer/api_explorer.py -a sell -m LRC-ETH -p 1 -v 100 55 | $python v3explorer/api_explorer.py -a buy -m LRC-ETH -p 0.9 -v 100 56 | ``` 57 | 58 | ### swap 59 | 60 | ```bash 61 | $python v3explorer/api_explorer.py -a swap-sell -n LRCETH-Pool -m LRC-ETH -p 1.0 -v 100 62 | $python v3explorer/api_explorer.py -a swap-buy -n LRCETH-Pool -m LRC-ETH -p 0.9 -v 100 63 | ``` 64 | 65 | ### report account 66 | ```bash 67 | $python v3explorer/api_explorer.py -a report 68 | ``` 69 | 70 | ### query account logs 71 | ```bash 72 | $python v3explorer/api_explorer.py -a query -T transfers 73 | $python v3explorer/api_explorer.py -a query -T orders 74 | $python v3explorer/api_explorer.py -a query -T amm 75 | ... 76 | ``` 77 | -------------------------------------------------------------------------------- /v3explorer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loopring/hello_loopring/456862046542fcb75ac020f42bc3a3f08e96ac91/v3explorer/__init__.py -------------------------------------------------------------------------------- /v3explorer/api_explorer.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from decimal import Decimal 3 | import sys 4 | from time import sleep 5 | 6 | from sdk.ethsnarks.eddsa import PoseidonEdDSA 7 | from sdk.loopring_v3_client import LoopringV3Client 8 | 9 | 10 | loopring_exported_account = { 11 | "name" : "UAT Account 1", 12 | "chainId": 5, 13 | "exchangeName": "LoopringDEX: V2", 14 | "exchangeAddress": "0x12b7cccF30ba360e5041C6Ce239C9a188B709b2B", 15 | "accountAddress": "0x727e0fa09389156fc803eaf9c7017338efd76e7f", 16 | "accountId": 10037, 17 | "apiKey": "JC1vbpMLprvoALsNFuhyCkVsTwZa2TSyMsP9fW1jLhR6yMbWcygnrcxcfHNnnRHk", 18 | "publicKeyX": "0x1959921f5f8d4f486b4c57afd6459444be34c75eede50ff1bd00b0333887eb70", 19 | "publicKeyY": "0x2bb25e133b1294626e1154e662b38d9332b66927451813c1615912fd705b7720", 20 | "privateKey": "0x5db65ed466a3b154dcf83e2e4b06b66c0c305d7d2088f9f60031567cf080dc1", 21 | "ecdsaKey" : "491aecdb1d5f6400a6b62fd12a41a86715bbab675c37a4060ba115fecf94083c", 22 | "whitelisted": False 23 | } 24 | 25 | if __name__ == "__main__": 26 | parser = argparse.ArgumentParser(description="Loopring DEX Rest API 3.6 Trading Example") 27 | parser.add_argument("-a", "--action", required=True, 28 | choices=[ 29 | 'time', 'apiKey', 30 | #order 31 | 'buy', 'sell', 'cancel', 32 | #offchain requests 33 | 'transfer', 'withdraw', 'update', 34 | #amm 35 | 'join', 'exit', 'swap-buy', 'swap-sell', 36 | #misc 37 | 'report', 'query', 38 | #on-chain op 39 | 'deposit' 40 | ], 41 | default='time', help='choose action') 42 | parser.add_argument("-m", "--market", default="LRC-USDT", help='specific token market') 43 | parser.add_argument("-n", "--poolName", default="LRC-USDT", help='specific AMM pool name') 44 | parser.add_argument("-d", "--direction", default="0", help='direction of swap') 45 | parser.add_argument("-t", "--token", default="LRC", help='specific token to transfer') 46 | parser.add_argument("-k", "--key", default="0x4a3d1e098350", help='specific eddsa key tobe updated') 47 | parser.add_argument("-u", "--user_to", default="", help='specific user_to account address to transfer') 48 | parser.add_argument("-p", "--price", help='order price or prices(, splited prices) if batch mode') 49 | parser.add_argument("-v", "--volume", help='order volume or volumes(, splited volumes) if batch mode') 50 | parser.add_argument("-b", "--burnOrmint", help='token to be burn/mint') 51 | parser.add_argument("-O", "--orderid", help='order id to be cancelled') 52 | parser.add_argument("-H", "--orderhash", help='order hash to be cancelled') 53 | parser.add_argument("-T", "--queryType", help='operation type to be approved') 54 | 55 | args = parser.parse_args() 56 | 57 | loopring_rest_sample = LoopringV3Client() 58 | if args.action == "time": 59 | srv_time = loopring_rest_sample.query_srv_time() 60 | print(f"srv time is {srv_time}") 61 | else: 62 | loopring_rest_sample.connect(loopring_exported_account) 63 | if args.action == "buy": 64 | buy_token, sell_token = args.market.split('-') 65 | price = float(args.price) 66 | volume = float(args.volume) 67 | loopring_rest_sample.send_order(buy_token, sell_token, True, price, volume) 68 | elif args.action == "sell": 69 | buy_token, sell_token = args.market.split('-') 70 | price = float(args.price) 71 | volume = float(args.volume) 72 | loopring_rest_sample.send_order(buy_token, sell_token, False, price, volume) 73 | elif args.action == "cancel": 74 | cancal_params = {} 75 | if args.orderhash: 76 | cancal_params['orderHash'] = args.orderhash 77 | elif args.orderid: 78 | cancal_params['clientOrderId'] = args.orderid 79 | loopring_rest_sample.cancel_order(**cancal_params) 80 | if args.action == "transfer": 81 | token = args.token 82 | to = args.user_to 83 | amount = Decimal(args.volume) 84 | loopring_rest_sample.transfer_eddsa(to, token, amount) 85 | elif args.action == "withdraw": 86 | token = args.token 87 | to = args.user_to if args.user_to != "" else args.user_to 88 | amount = Decimal(args.volume) 89 | loopring_rest_sample.offchainWithdraw_eddsa(to, token, amount, 0, bytes(0)) 90 | elif args.action == "update": 91 | privkey = int(args.key, 16) 92 | pubKey = PoseidonEdDSA.B() * privkey 93 | # print(pubKey) 94 | loopring_rest_sample.update_account_eddsa(privkey, pubKey) 95 | elif args.action == 'join': 96 | joinAmounts = [Decimal(v) for v in args.volume.split(',')] 97 | mint = Decimal(args.burnOrmint) 98 | poolName = args.market 99 | poolAddr = loopring_rest_sample.ammPoolNames[poolName] 100 | loopring_rest_sample.join_amm_pool(poolName, joinAmounts, mint) 101 | elif args.action == 'exit': 102 | exitAmounts = [Decimal(v) for v in args.volume.split(',')] 103 | burn = Decimal(args.burnOrmint) 104 | token = args.token 105 | poolName = args.market 106 | poolAddr = loopring_rest_sample.ammPoolNames[poolName] 107 | loopring_rest_sample.exit_amm_pool(poolName, burn, exitAmounts) 108 | elif args.action == 'swap-buy': 109 | buy_token, sell_token = args.market.split('-') 110 | poolName = args.poolName 111 | poolAddr = loopring_rest_sample.ammPoolNames[poolName] 112 | price = float(args.price) 113 | volume = float(args.volume) 114 | loopring_rest_sample.send_order(buy_token, sell_token, True, price, volume, poolAddr) 115 | elif args.action == 'swap-sell': 116 | buy_token, sell_token = args.market.split('-') 117 | poolName = args.poolName 118 | poolAddr = loopring_rest_sample.ammPoolNames[poolName] 119 | price = float(args.price) 120 | volume = float(args.volume) 121 | loopring_rest_sample.send_order(buy_token, sell_token, False, price, volume, poolAddr) 122 | elif args.action == 'report': 123 | loopring_rest_sample.get_account() 124 | loopring_rest_sample.get_apiKey() 125 | loopring_rest_sample.query_balance() 126 | elif args.action == 'query': 127 | # hash = args.orderhash 128 | if args.queryType == 'orders': 129 | loopring_rest_sample.get_orders() 130 | elif args.queryType == 'transfers': 131 | loopring_rest_sample.get_transfers() 132 | elif args.queryType == 'trades': 133 | loopring_rest_sample.get_trades() 134 | elif args.queryType == 'withdrawals': 135 | loopring_rest_sample.get_withdrawals() 136 | elif args.queryType in ['join', 'exit', 'amm']: 137 | loopring_rest_sample.get_amm_txs() 138 | elif args.queryType == 'pools': 139 | loopring_rest_sample.query_info("amm/pools") 140 | elif args.queryType == 'markets': 141 | loopring_rest_sample.query_info("exchange/info") 142 | sleep(5) 143 | --------------------------------------------------------------------------------