Port parts of several other modules
Focus on getting most types defined with minimal implementation and fields sufficient for simple tests
This commit is contained in:
parent
e1c04e587f
commit
84ccfc5966
|
@ -0,0 +1,10 @@
|
|||
import
|
||||
logging, constants, utils/header
|
||||
|
||||
type
|
||||
CountableList*[T] = ref object
|
||||
elements: seq[T] # TODO
|
||||
|
||||
Block* = ref object of RootObj
|
||||
header*: Header
|
||||
uncles*: CountableList[Header]
|
|
@ -0,0 +1,60 @@
|
|||
import
|
||||
tables,
|
||||
logging, constants, errors, validation, utils / [hexadecimal]
|
||||
|
||||
type
|
||||
BlockHeader* = ref object
|
||||
# Placeholder TODO
|
||||
|
||||
Chain* = ref object
|
||||
## An Chain is a combination of one or more VM classes. Each VM is associated
|
||||
## with a range of blocks. The Chain class acts as a wrapper around these other
|
||||
## VM classes, delegating operations to the appropriate VM depending on the
|
||||
## current block number.
|
||||
header*: BlockHeader
|
||||
logger*: Logger
|
||||
networkId*: cstring
|
||||
vmsByRange*: seq[tuple[blockNumber: Int256, vm: VM]] # TODO
|
||||
importBlock*: bool
|
||||
validateBlock*: bool
|
||||
db*: BaseChainDB
|
||||
|
||||
GenesisParams* = ref object
|
||||
blockNumber*: Int256
|
||||
difficulty*: Int256
|
||||
gasLimit*: Int256
|
||||
parentHash*: cstring
|
||||
coinbase*: cstring
|
||||
nonce: cstring
|
||||
mixHash: cstring
|
||||
extraData: cstring
|
||||
timestamp: int,
|
||||
stateRoot: cstring
|
||||
|
||||
FundedAddress* = ref object
|
||||
balance*: Int256
|
||||
nonce*: int
|
||||
code*: cstring
|
||||
|
||||
|
||||
proc configureChain*(name: string, blockNumber: Int256, vm: VM, importBlock: bool = true, validateBlock: bool = true): Chain =
|
||||
new(result)
|
||||
result.vmsByRange = @[(blockNumber: blockNumber, vm: vm)]
|
||||
|
||||
proc fromGenesis*(
|
||||
chain: Chain,
|
||||
chainDB: BaseChainDB,
|
||||
genesisParams: GenesisParams,
|
||||
genesisState: Table[string, FundedAddress]): Chain =
|
||||
## Initialize the Chain from a genesis state
|
||||
var stateDB = chaindb.getStateDB(BLANK_ROOT_HASH)
|
||||
for account, accountData in genesisState:
|
||||
stateDB.setBalance(account, accountData.balance)
|
||||
stateDB.setNonce(account, accountData.nonce)
|
||||
stateDB.setCode(account, accountData.code)
|
||||
|
||||
new(result)
|
||||
result.db = chainDB
|
||||
result.header = BlockHeader()
|
||||
result.logger = logging.getLogger("evm.chain.chain.Chain")
|
||||
chainDB.persistBlockToDB(result.getBlock())
|
|
@ -114,7 +114,7 @@ const
|
|||
UINT_256_CEILING* = (2 ^ X).Int256
|
||||
UINT_255_MAX* = (2 ^ (X - 1) - 1).Int256
|
||||
UINT_255_CEILING* = (2 ^ (X - 1)).Int256
|
||||
NULLBYTE = cstring"\\x00"
|
||||
NULLBYTE = cstring"\x00"
|
||||
EMPTYWORD = repeat(NULLBYTE, 32)
|
||||
# UINT160CEILING = 2 ^ 160
|
||||
CREATE_CONTRACT_ADDRESS* = cstring""
|
||||
|
@ -190,15 +190,16 @@ const
|
|||
SECPK1Gx = 0
|
||||
SECPK1Gy = 0
|
||||
SECPK1G = (SECPK1Gx, SECPK1Gy)
|
||||
EMPTYUNCLEHASH = cstring"\\x1d\\xccM\\xe8\\xde\\xc7]z\\xab\\x85\\xb5g\\xb6\\xcc\\xd4\\x1a\\xd3\\x12E\\x1b\\x94\\x8at\\x13\\xf0\\xa1B\\xfd@\\xd4\\x93G"
|
||||
EMPTYUNCLEHASH = cstring"\x1d\xccM\xe8\xde\xc7]z\xab\x85\xb5g\xb6\xcc\xd4\x1a\xd3\x12E\x1b\x94\x8at\x13\xf0\xa1B\xfd@\xd4\x93G"
|
||||
GENESIS_BLOCK_NUMBER* = 0.Int256
|
||||
GENESIS_DIFFICULTY* = 131072.Int256
|
||||
GENESISGASLIMIT = 3141592
|
||||
GENESISPARENTHASH = ZEROHASH32
|
||||
GENESISCOINBASE = ZEROADDRESS
|
||||
GENESISNONCE = cstring"\\x00\\x00\\x00\\x00\\x00\\x00\\x00B"
|
||||
GENESISMIXHASH = ZEROHASH32
|
||||
EMPTYSHA3 = cstring"\\xc5\\xd2F\\x01\\x86\\xf7#<\\x92~}\\xb2\\xdc\\xc7\\x03\\xc0\\xe5\\x00\\xb6S\\xca\\x82';{\\xfa\\xd8\\x04]\\x85\\xa4p"
|
||||
BLANKROOTHASH = cstring"V\\xe8\\x1f\\x17\\x1b\\xccU\\xa6\\xff\\x83E\\xe6\\x92\\xc0\\xf8n[H\\xe0\\x1b\\x99l\\xad\\xc0\\x01b/\\xb5\\xe3c\\xb4!"
|
||||
GASMODEXPQUADRATICDENOMINATOR = 20
|
||||
MAXPREVHEADERDEPTH = 256
|
||||
GENESIS_GAS_LIMIT* = 3141592.Int256
|
||||
GENESIS_PARENT_HASH* = ZERO_HASH32
|
||||
GENESIS_COINBASE* = ZERO_ADDRESS
|
||||
GENESIS_NONCE* = cstring"\x00\x00\x00\x00\x00\x00\x00B"
|
||||
GENESIS_MIX_HASH* = ZERO_HASH32
|
||||
GENESIS_EXTRA_DATA = cstring""
|
||||
EMPTYSHA3 = cstring"\xc5\xd2F\x01\x86\xf7#<\x92~}\xb2\xdc\xc7\x03\xc0\xe5\x00\xb6S\xca\x82';{\xfa\xd8\x04]\x85\xa4p"
|
||||
BLANK_ROOT_HASH* = cstring"V\xe8\x1f\x17\x1b\xccU\xa6\xff\x83E\xe6\x92\xc0\xf8n[H\xe0\x1b\x99l\xad\xc0\x01b/\xb5\xe3c\xb4!"
|
||||
GAS_MOD_EXP_QUADRATIC_DENOMINATOR* = 20
|
||||
MAX_PREV_HEADER_DEPTH* = 256
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
type
|
||||
BaseChainDB* = ref object
|
||||
# TODO db*: JournalDB
|
||||
|
||||
# proc makeBaseChainDB*(db: MemoryDB): BaseChainDB =
|
||||
# result.db = JournalDB(db)
|
||||
|
||||
# proc exists*(self: BaseChainDB; key: cstring): bool =
|
||||
# return self.db.exists(key)
|
||||
|
||||
# proc getCanonicalHead*(self: BaseChainDB): BlockHeader =
|
||||
# if notself.exists(CANONICALHEADHASHDBKEY):
|
||||
# raise newException(CanonicalHeadNotFound,
|
||||
# "No canonical head set for this chain")
|
||||
# return self.getBlockHeaderByHash(self.db.get(CANONICALHEADHASHDBKEY))
|
||||
|
||||
# proc getCanonicalBlockHeaderByNumber*(self: BaseChainDB; blockNumber: int): BlockHeader =
|
||||
# ## Returns the block header with the given number in the canonical chain.
|
||||
# ##
|
||||
# ## Raises BlockNotFound if there's no block header with the given number in the
|
||||
# ## canonical chain.
|
||||
# validateUint256(blockNumber)
|
||||
# return self.getBlockHeaderByHash(self.lookupBlockHash(blockNumber))
|
||||
|
||||
# proc getScore*(self: BaseChainDB; blockHash: cstring): int =
|
||||
# return rlp.decode(self.db.get(makeBlockHashToScoreLookupKey(blockHash)))
|
||||
|
||||
# proc setAsCanonicalChainHead*(self: BaseChainDB; header: BlockHeader): void =
|
||||
# ## Sets the header as the canonical chain HEAD.
|
||||
# for h in reversed(self.findCommonAncestor(header)):
|
||||
# self.addBlockNumberToHashLookup(h)
|
||||
# try:
|
||||
# self.getBlockHeaderByHash(header.hash)
|
||||
# except BlockNotFound:
|
||||
# raise newException(ValueError, "Cannot use unknown block hash as canonical head: {}".format(
|
||||
# header.hash))
|
||||
# self.db.set(CANONICALHEADHASHDBKEY, header.hash)
|
||||
|
||||
# iterator findCommonAncestor*(self: BaseChainDB; header: BlockHeader): BlockHeader =
|
||||
# ## Returns the chain leading up from the given header until the first ancestor it has in
|
||||
# ## common with our canonical chain.
|
||||
# var h = header
|
||||
# while true:
|
||||
# yield h
|
||||
# if h.parentHash == GENESISPARENTHASH:
|
||||
# break
|
||||
# try:
|
||||
# var orig = self.getCanonicalBlockHeaderByNumber(h.blockNumber)
|
||||
# except KeyError:
|
||||
# nil
|
||||
# h = self.getBlockHeaderByHash(h.parentHash)
|
||||
|
||||
# proc getBlockHeaderByHash*(self: BaseChainDB; blockHash: cstring): BlockHeader =
|
||||
# ## Returns the requested block header as specified by block hash.
|
||||
# ##
|
||||
# ## Raises BlockNotFound if it is not present in the db.
|
||||
# validateWord(blockHash)
|
||||
# try:
|
||||
# var block = self.db.get(blockHash)
|
||||
# except KeyError:
|
||||
# raise newException(BlockNotFound, "No block with hash {0} found".format(
|
||||
# encodeHex(blockHash)))
|
||||
# return rlp.decode(block)
|
||||
|
||||
# proc headerExists*(self: BaseChainDB; blockHash: cstring): bool =
|
||||
# ## Returns True if the header with the given block hash is in our DB.
|
||||
# return self.db.exists(blockHash)
|
||||
|
||||
# proc lookupBlockHash*(self: BaseChainDB; blockNumber: int): cstring =
|
||||
# ## Return the block hash for the given block number.
|
||||
# validateUint256(blockNumber)
|
||||
# var
|
||||
# numberToHashKey = makeBlockNumberToHashLookupKey(blockNumber)
|
||||
# blockHash = rlp.decode(self.db.get(numberToHashKey))
|
||||
# return blockHash
|
||||
|
||||
# iterator getReceipts*(self: BaseChainDB; header: BlockHeader; receiptClass: typedesc): Receipt =
|
||||
# var receiptDb = HexaryTrie()
|
||||
# for receiptIdx in itertools.count():
|
||||
# var receiptKey = rlp.encode(receiptIdx)
|
||||
# if receiptKey in receiptDb:
|
||||
# var receiptData = receiptDb[receiptKey]
|
||||
# yield rlp.decode(receiptData)
|
||||
# else:
|
||||
# break
|
||||
|
||||
# iterator getBlockTransactions*(self: BaseChainDB; blockHeader: BlockHeader;
|
||||
# transactionClass: typedesc): FrontierTransaction =
|
||||
# var transactionDb = HexaryTrie(self.db)
|
||||
# for transactionIdx in itertools.count():
|
||||
# var transactionKey = rlp.encode(transactionIdx)
|
||||
# if transactionKey in transactionDb:
|
||||
# var transactionData = transactionDb[transactionKey]
|
||||
# yield rlp.decode(transactionData)
|
||||
# else:
|
||||
# break
|
||||
|
||||
# proc addBlockNumberToHashLookup*(self: BaseChainDB; header: BlockHeader): void =
|
||||
# var blockNumberToHashKey = makeBlockNumberToHashLookupKey(header.blockNumber)
|
||||
# self.db.set(blockNumberToHashKey, rlp.encode(header.hash))
|
||||
|
||||
# proc persistHeaderToDb*(self: BaseChainDB; header: BlockHeader): void =
|
||||
# if header.parentHash != GENESISPARENTHASH and
|
||||
# notself.headerExists(header.parentHash):
|
||||
# raise newException(ParentNotFound, "Cannot persist block header ({}) with unknown parent ({})".format(
|
||||
# encodeHex(header.hash), encodeHex(header.parentHash)))
|
||||
# self.db.set(header.hash, rlp.encode(header))
|
||||
# if header.parentHash == GENESISPARENTHASH:
|
||||
# var score = header.difficulty
|
||||
# else:
|
||||
# score = self.getScore(header.parentHash) + header.difficulty
|
||||
# self.db.set(makeBlockHashToScoreLookupKey(header.hash), rlp.encode(score))
|
||||
# try:
|
||||
# var headScore = self.getScore(self.getCanonicalHead().hash)
|
||||
# except CanonicalHeadNotFound:
|
||||
# self.setAsCanonicalChainHead(header)
|
||||
|
||||
# proc persistBlockToDb*(self: BaseChainDB; block: FrontierBlock): void =
|
||||
# self.persistHeaderToDb(block.header)
|
||||
# var transactionDb = HexaryTrie(self.db)
|
||||
# for i in 0 ..< len(block.transactions):
|
||||
# var indexKey = rlp.encode(i)
|
||||
# transactionDb[indexKey] = rlp.encode(block.transactions[i])
|
||||
# nil
|
||||
# self.db.set(block.header.unclesHash, rlp.encode(block.uncles))
|
||||
|
||||
# proc addTransaction*(self: BaseChainDB; blockHeader: BlockHeader; indexKey: cstring;
|
||||
# transaction: FrontierTransaction): cstring =
|
||||
# var transactionDb = HexaryTrie(self.db)
|
||||
# transactionDb[indexKey] = rlp.encode(transaction)
|
||||
# return transactionDb.rootHash
|
||||
|
||||
# proc addReceipt*(self: BaseChainDB; blockHeader: BlockHeader; indexKey: cstring;
|
||||
# receipt: Receipt): cstring =
|
||||
# var receiptDb = HexaryTrie()
|
||||
# receiptDb[indexKey] = rlp.encode(receipt)
|
||||
# return receiptDb.rootHash
|
||||
|
||||
# proc snapshot*(self: BaseChainDB): UUID =
|
||||
# return self.db.snapshot()
|
||||
|
||||
# proc commit*(self: BaseChainDB; checkpoint: UUID): void =
|
||||
# self.db.commit(checkpoint)
|
||||
|
||||
# proc clear*(self: BaseChainDB): void =
|
||||
# self.db.clear()
|
||||
|
||||
# proc getStateDb*(self: BaseChainDB; stateRoot: cstring; readOnly: bool): AccountStateDB =
|
||||
# return AccountStateDB()
|
||||
|
||||
# var CANONICALHEADHASHDBKEY = cstring"v1:canonical_head_hash"
|
|
@ -0,0 +1,32 @@
|
|||
import
|
||||
constants, errors
|
||||
|
||||
type
|
||||
BaseTransaction* = ref object
|
||||
nonce*: Int256
|
||||
gasPrice*: Int256
|
||||
gas*: Int256
|
||||
to*: cstring
|
||||
value*: Int256
|
||||
data*: cstring
|
||||
v*: Int256
|
||||
r*: Int256
|
||||
s*: Int256
|
||||
|
||||
proc intrinsicGas*(t: BaseTransaction): Int256 =
|
||||
# Compute the baseline gas cost for this transaction. This is the amount
|
||||
# of gas needed to send this transaction (but that is not actually used
|
||||
# for computation)
|
||||
raise newException(ValueError, "not implemented intrinsicGas")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
proc validate*(t: BaseTransaction) =
|
||||
# Hook called during instantiation to ensure that all transaction
|
||||
# parameters pass validation rules
|
||||
if t.intrinsic_gas() > t.gas:
|
||||
raise newException(ValidationError, "Insufficient gas")
|
||||
# self.check_signature_validity()
|
|
@ -0,0 +1,37 @@
|
|||
type
|
||||
Header* = ref object
|
||||
# TODO
|
||||
|
||||
proc generateHeaderFromParentHeader*(
|
||||
computeDifficultyFn: proc(parentHeader: Header, timestamp: int): int,
|
||||
parentHeader: Header,
|
||||
coinbase: cstring,
|
||||
timestamp: int = -1,
|
||||
extraData: cstring = cstring""): Header =
|
||||
Header()
|
||||
# Generate BlockHeader from state_root and parent_header
|
||||
# if timestamp is None:
|
||||
# timestamp = max(int(time.time()), parent_header.timestamp + 1)
|
||||
# elif timestamp <= parent_header.timestamp:
|
||||
# raise ValueError(
|
||||
# "header.timestamp ({}) should be higher than"
|
||||
# "parent_header.timestamp ({})".format(
|
||||
# timestamp,
|
||||
# parent_header.timestamp,
|
||||
# )
|
||||
# )
|
||||
# header = BlockHeader(
|
||||
# difficulty=compute_difficulty_fn(parent_header, timestamp),
|
||||
# block_number=(parent_header.block_number + 1),
|
||||
# gas_limit=compute_gas_limit(
|
||||
# parent_header,
|
||||
# gas_limit_floor=GENESIS_GAS_LIMIT,
|
||||
# ),
|
||||
# timestamp=timestamp,
|
||||
# parent_hash=parent_header.hash,
|
||||
# state_root=parent_header.state_root,
|
||||
# coinbase=coinbase,
|
||||
# extra_data=extra_data,
|
||||
# )
|
||||
|
||||
# return header
|
|
@ -0,0 +1,61 @@
|
|||
import
|
||||
../logging, ../constants, ../errors, ../transaction, ../computation, "../block", ../vm_state, ../db/chain, ../utils/db, ../utils/header
|
||||
|
||||
type
|
||||
VM* = ref object of RootObj
|
||||
# The VM class represents the Chain rules for a specific protocol definition
|
||||
# such as the Frontier or Homestead network. Defining an Chain defining
|
||||
# individual VM classes for each fork of the protocol rules within that
|
||||
# network
|
||||
|
||||
chainDB*: BaseChainDB
|
||||
isStateless*: bool
|
||||
state*: BaseVMState
|
||||
|
||||
proc newVM*(header: Header, chainDB: BaseChainDB): VM =
|
||||
new(result)
|
||||
result.chainDB = chainDB
|
||||
|
||||
|
||||
method addTransaction*(vm: var VM, transaction: BaseTransaction, computation: BaseComputation): Block =
|
||||
# Add a transaction to the given block and save the block data into chaindb
|
||||
var receipt = self.state.makeReceipt(transaction, computation)
|
||||
var transactionIdx = len(vm.`block`.transactions)
|
||||
return Block()
|
||||
# var indexKey = rlp.encode(transaction_idx, sedes=rlp.sedes.big_endian_int)
|
||||
|
||||
# self.block.transactions.append(transaction)
|
||||
|
||||
# tx_root_hash = self.chaindb.add_transaction(self.block.header, index_key, transaction)
|
||||
# receipt_root_hash = self.chaindb.add_receipt(self.block.header, index_key, receipt)
|
||||
|
||||
# self.block.bloom_filter |= receipt.bloom
|
||||
|
||||
# self.block.header.transaction_root = tx_root_hash
|
||||
# self.block.header.receipt_root = receipt_root_hash
|
||||
# self.block.header.bloom = int(self.block.bloom_filter)
|
||||
# self.block.header.gas_used = receipt.gas_used
|
||||
|
||||
# return self.block
|
||||
|
||||
method applyTransaction*(vm: var VM, transaction: BaseTransaction): (BaseComputation, Block) =
|
||||
# Apply the transaction to the vm in the current block
|
||||
if vm.isStateless:
|
||||
var (computation, b, trieData) = vm.state.applyTransaction(
|
||||
vm.state,
|
||||
transaction,
|
||||
vm.`block`,
|
||||
isStateless=true)
|
||||
vm.`block` = b
|
||||
result = (computation, b)
|
||||
# Persist changed transaction and receipt key-values to self.chaindb.
|
||||
else:
|
||||
var (computation, _, _) = vm.state.applyTransaction(
|
||||
vm.state,
|
||||
transaction,
|
||||
vm.`block`,
|
||||
isStateless=false)
|
||||
vm.addTransaction(transaction, computation)
|
||||
|
||||
result = (computation, vm.`block`)
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import
|
||||
strformat,
|
||||
logging, constants, errors, utils/state
|
||||
strformat, tables,
|
||||
logging, constants, errors, transaction, db/chain, utils/state, utils/header
|
||||
|
||||
type
|
||||
BaseVMState* = ref object of RootObj
|
||||
prevHeaders*: bool
|
||||
receipts*: bool
|
||||
computationClass*: bool
|
||||
chaindb*: bool
|
||||
accessLogs*: seq[bool]
|
||||
blockHeader*: bool
|
||||
prevHeaders*: seq[Header]
|
||||
receipts*: seq[string]
|
||||
# computationClass*: bool
|
||||
chaindb*: BaseChainDB
|
||||
accessLogs*: seq[string]
|
||||
blockHeader*: Header
|
||||
name*: string
|
||||
|
||||
proc newBaseVMState*: BaseVMState =
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import
|
||||
strformat, tables,
|
||||
logging, constants, errors, computation, transaction, vm_state, "block", db/chain, utils/state, utils/header
|
||||
|
||||
method executeTransaction(vmState: var BaseVMState, transaction: BaseTransaction): (BaseComputation, Header) =
|
||||
# Execute the transaction in the vm
|
||||
raise newException(ValueError, "Must be implemented by subclasses")
|
||||
|
||||
|
||||
method addTransaction*(vmState: var BaseVMState, transaction: BaseTransaction, computation: BaseComputation, b: Block): (Block, Table[cstring, cstring]) =
|
||||
# Add a transaction to the given block and
|
||||
# return `trieData` to store the transaction data in chaindb in VM layer
|
||||
# Update the bloomFilter, transaction trie and receipt trie roots, bloom_filter,
|
||||
# bloom, and usedGas of the block
|
||||
# transaction: the executed transaction
|
||||
# computation: the Computation object with executed result
|
||||
# block: the Block which the transaction is added in
|
||||
# var receipt = vmState.makeReceipt(transaction, computation)
|
||||
# vmState.add_receipt(receipt)
|
||||
|
||||
# block.transactions.append(transaction)
|
||||
|
||||
# # Get trie roots and changed key-values.
|
||||
# tx_root_hash, tx_kv_nodes = make_trie_root_and_nodes(block.transactions)
|
||||
# receipt_root_hash, receipt_kv_nodes = make_trie_root_and_nodes(self.receipts)
|
||||
|
||||
# trie_data = merge(tx_kv_nodes, receipt_kv_nodes)
|
||||
|
||||
# block.bloom_filter |= receipt.bloom
|
||||
|
||||
# block.header.transaction_root = tx_root_hash
|
||||
# block.header.receipt_root = receipt_root_hash
|
||||
# block.header.bloom = int(block.bloom_filter)
|
||||
# block.header.gas_used = receipt.gas_used
|
||||
|
||||
# return block, trie_data
|
||||
result = (b, initTable[cstring, cstring]())
|
||||
|
||||
method applyTransaction*(
|
||||
vmState: var BaseVMState,
|
||||
transaction: BaseTransaction,
|
||||
b: Block,
|
||||
isStateless: bool): (BaseComputation, Block, Table[cstring, cstring]) =
|
||||
# Apply transaction to the given block
|
||||
# transaction: the transaction need to be applied
|
||||
# b: the block which the transaction applies on
|
||||
# isStateless: if isStateless, call vmState.addTransaction to set block
|
||||
|
||||
if isStateless:
|
||||
var ourBlock = b # deepcopy
|
||||
vmState.blockHeader = b.header
|
||||
var (computation, blockHeader) = vmState.executeTransaction(transaction)
|
||||
|
||||
ourBlock.header = blockHeader
|
||||
var trieData: Table[cstring, cstring]
|
||||
(ourBlock, trieData) = vmState.addTransaction(transaction, computation, ourBlock)
|
||||
|
||||
result = (computation, ourBlock, trieData)
|
||||
else:
|
||||
var (computation, blockHeader) = vmState.executeTransaction(transaction)
|
||||
return (computation, nil, initTable[cstring, cstring]())
|
Loading…
Reference in New Issue