Merge branch 'generalstate'

This commit is contained in:
Ștefan Talpalaru 2018-12-07 18:37:12 +01:00
commit d633bb9ebf
No known key found for this signature in database
GPG Key ID: CBF7934204F1B6F9
12 changed files with 1281 additions and 1173 deletions

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,8 @@
import
parseopt, strutils, macros, os,
asyncdispatch2, eth_keys, eth_p2p, eth_common, chronicles, nimcrypto/hash
asyncdispatch2, eth_keys, eth_p2p, eth_common, chronicles, nimcrypto/hash,
./vm/interpreter/vm_forks
const
NimbusName* = "Nimbus"
@ -175,14 +176,14 @@ proc publicChainConfig*(id: PublicNetwork): ChainConfig =
of MainNet:
ChainConfig(
chainId: MainNet.uint,
homesteadBlock: 1150000.u256,
daoForkBlock: 1920000.u256,
homesteadBlock: forkBlocks[FkHomestead],
daoForkBlock: forkBlocks[FkDao],
daoForkSupport: true,
eip150Block: 2463000.u256,
eip150Block: forkBlocks[FkTangerine],
eip150Hash: toDigest("2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0"),
eip155Block: 2675000.u256,
eip158Block: 2675000.u256,
byzantiumBlock: 4370000.u256
eip155Block: forkBlocks[FkSpurious],
eip158Block: forkBlocks[FkSpurious],
byzantiumBlock: forkBlocks[FkByzantium]
)
of RopstenNet:
ChainConfig(

View File

@ -6,7 +6,7 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
chronicles, strformat, strutils, sequtils, macros, terminal, math, tables,
chronicles, strformat, strutils, sequtils, macros, terminal, math, tables, options,
eth_common,
../constants, ../errors, ../validation, ../vm_state, ../vm_types,
./interpreter/[opcode_values, gas_meter, gas_costs, vm_forks],
@ -17,7 +17,7 @@ import
logScope:
topics = "vm computation"
proc newBaseComputation*(vmState: BaseVMState, blockNumber: UInt256, message: Message): BaseComputation =
proc newBaseComputation*(vmState: BaseVMState, blockNumber: UInt256, message: Message, forkOverride=none(Fork)): BaseComputation =
new result
result.vmState = vmState
result.msg = message
@ -29,7 +29,12 @@ proc newBaseComputation*(vmState: BaseVMState, blockNumber: UInt256, message: Me
result.logEntries = @[]
result.code = newCodeStream(message.code)
# result.rawOutput = "0x"
result.gasCosts = blockNumber.toFork.forkToSchedule
result.gasCosts =
if forkOverride.isSome:
forkOverride.get.forkToSchedule
else:
blockNumber.toFork.forkToSchedule
result.forkOverride = forkOverride
proc isOriginComputation*(c: BaseComputation): bool =
# Is this computation the computation initiated by a transaction
@ -209,7 +214,8 @@ proc generateChildComputation*(fork: Fork, computation: BaseComputation, childMs
var childComp = newBaseComputation(
computation.vmState,
computation.vmState.blockHeader.blockNumber,
childMsg)
childMsg,
some(fork))
# Copy the fork op code executor proc (assumes child computation is in the same fork)
childComp.opCodeExec = computation.opCodeExec
@ -235,9 +241,16 @@ proc addChildComputation(fork: Fork, computation: BaseComputation, child: BaseCo
computation.returnData = child.output
computation.children.add(child)
proc getFork*(computation: BaseComputation): Fork =
result =
if computation.forkOverride.isSome:
computation.forkOverride.get
else:
computation.vmState.blockHeader.blockNumber.toFork
proc applyChildComputation*(computation: BaseComputation, childMsg: Message, opCode: static[Op]): BaseComputation =
## Apply the vm message childMsg as a child computation.
let fork = computation.vmState.blockHeader.blockNumber.toFork
let fork = computation.getFork
result = fork.generateChildComputation(computation, childMsg, opCode)
fork.addChildComputation(computation, result)

View File

@ -13,7 +13,7 @@ import
# Gas Fee Schedule
# Yellow Paper Appendix G - https://ethereum.github.io/yellowpaper/paper.pdf
type
GasFeeKind = enum
GasFeeKind* = enum
GasZero, # Nothing paid for operations of the set Wzero.
GasBase, # Amount of gas to pay for operations of the set Wbase.
GasVeryLow, # Amount of gas to pay for operations of the set Wverylow.
@ -102,11 +102,13 @@ type
GasCosts* = array[Op, GasCost]
template gasCosts(FeeSchedule: GasFeeSchedule, prefix, ResultGasCostsName: untyped) =
template gasCosts(fork: Fork, prefix, ResultGasCostsName: untyped) =
## Generate the gas cost for each forks and store them in a const
## named `ResultGasCostsName`
const FeeSchedule = gasFees[fork]
# ############### Helper functions ##############################
func `prefix gasMemoryExpansion`(currentMemSize, memOffset, memLength: Natural): GasInt {.inline.} =
@ -151,7 +153,6 @@ template gasCosts(FeeSchedule: GasFeeSchedule, prefix, ResultGasCostsName: untyp
## Computes all but 1/64th
## L(n) ≡ n ⌊n/64⌋ - (floored(n/64))
# Introduced in EIP-150 - https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md
# TODO: deactivate it pre-EIP150
# Note: The all-but-one-64th calculation should occur after the memory expansion fee is taken
# https://github.com/ethereum/yellowpaper/pull/442
@ -291,9 +292,16 @@ template gasCosts(FeeSchedule: GasFeeSchedule, prefix, ResultGasCostsName: untyp
gasParams.c_memLength
)
# Cnew_account - TODO - pre-EIP158 zero-value call consumed 25000 gas
# https://github.com/ethereum/eips/issues/158
if gasParams.c_isNewAccount and not value.isZero:
# Cnew_account
if gasParams.c_isNewAccount:
if fork < FkSpurious:
# Pre-EIP161 all account creation calls consumed 25000 gas.
result.gasCost += static(FeeSchedule[GasNewAccount])
else:
# Afterwards, only those transfering value:
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-158.md
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-161.md
if not value.isZero:
result.gasCost += static(FeeSchedule[GasNewAccount])
# Cxfer
@ -305,7 +313,10 @@ template gasCosts(FeeSchedule: GasFeeSchedule, prefix, ResultGasCostsName: untyp
let cextra = result.gasCost
# Cgascap
result.gasCost = if gasParams.c_gasBalance >= result.gasCost:
if fork >= FkTangerine:
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md
result.gasCost =
if gasParams.c_gasBalance >= result.gasCost:
min(
`prefix all_but_one_64th`(gasParams.c_gasBalance - result.gasCost),
gasParams.c_contract_gas
@ -574,9 +585,20 @@ const
TangerineGasFees = HomesteadGasFees.tangerineGasFees
SpuriousGasFees = TangerineGasFees.spuriousGasFees
gasCosts(BaseGasFees, base, BaseGasCosts)
gasCosts(HomesteadGasFees, homestead, HomesteadGasCosts)
gasCosts(TangerineGasFees, tangerine, TangerineGasCosts)
gasFees*: array[Fork, GasFeeSchedule] = [
FkFrontier: BaseGasFees,
FkThawing: BaseGasFees,
FkHomestead: HomesteadGasFees,
FkDao: HomesteadGasFees,
FkTangerine: TangerineGasFees,
FkSpurious: SpuriousGasFees,
FkByzantium: SpuriousGasFees, # not supported yet
]
gasCosts(FkFrontier, base, BaseGasCosts)
gasCosts(FkHomestead, homestead, HomesteadGasCosts)
gasCosts(FkTangerine, tangerine, TangerineGasCosts)
proc forkToSchedule*(fork: Fork): GasCosts =
if fork < FkHomestead:

View File

@ -522,7 +522,7 @@ op create, inline = false, value, startPosition, size:
computation.vmState.blockHeader.rlphash, false).
getBalance(computation.msg.sender)
if senderBalance >= value:
if senderBalance < value:
debug "Computation Failure", reason = "Insufficient funds available to transfer", required = computation.msg.value, balance = senderBalance
push: 0
return

View File

@ -9,7 +9,6 @@ import stint
type
Fork* = enum
# FkGenesis
FkFrontier,
FkThawing,
FkHomestead,
@ -18,9 +17,8 @@ type
FkSpurious,
FkByzantium
UInt256Pair = tuple[a: Uint256, b: Uint256]
let forkBlocks: array[Fork, Uint256] = [
const
forkBlocks*: array[Fork, Uint256] = [
FkFrontier: 1.u256, # 30/07/2015 19:26:28
FkThawing: 200_000.u256, # 08/09/2015 01:33:09
FkHomestead: 1_150_000.u256, # 14/03/2016 20:49:53
@ -28,7 +26,7 @@ let forkBlocks: array[Fork, Uint256] = [
FkTangerine: 2_463_000.u256, # 18/10/2016 17:19:31
FkSpurious: 2_675_000.u256, # 22/11/2016 18:15:44
FkByzantium: 4_370_000.u256 # 16/10/2017 09:22:11
]
]
proc toFork*(blockNumber: UInt256): Fork =

View File

@ -17,7 +17,7 @@ func invalidInstruction*(computation: var BaseComputation) {.inline.} =
raise newException(ValueError, "Invalid instruction, received an opcode not implemented in the current fork.")
let FrontierOpDispatch {.compileTime.}: array[Op, NimNode] = block:
fill_enum_table_holes(Op, newIdentNode"invalidInstruction"):
fill_enum_table_holes(Op, newIdentNode("invalidInstruction")):
[
Stop: newIdentNode "toBeReplacedByBreak",
Add: newIdentNode "add",
@ -235,20 +235,22 @@ proc frontierVM(computation: var BaseComputation) =
proc updateOpcodeExec*(computation: var BaseComputation, fork: Fork) =
case fork
of FkFrontier:
of FkFrontier..FkSpurious:
computation.opCodeExec = frontierVM
computation.frontierVM()
else:
raise newException(VMError, "Unknown or not implemented fork: " & $fork)
proc updateOpcodeExec*(computation: var BaseComputation) =
let fork = computation.vmState.blockHeader.blockNumber.toFork
let fork = computation.getFork
computation.updateOpcodeExec(fork)
proc executeOpcodes*(computation: var BaseComputation) =
# TODO: Optimise getting fork and updating opCodeExec only when necessary
let fork = computation.vmState.blockHeader.blockNumber.toFork
let fork = computation.getFork
try:
computation.updateOpcodeExec(fork)
except VMError:
computation.error = Error(info: getCurrentExceptionMsg())
debug "executeOpcodes() failed", error = getCurrentExceptionMsg()

View File

@ -6,11 +6,11 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
ranges/typedranges, sequtils, strformat, tables,
ranges/typedranges, sequtils, strformat, tables, options,
eth_common, chronicles,
./constants, ./errors, ./vm/computation,
./transaction, ./vm_types, ./vm_state, ./block_types, ./db/[db_chain, state_db], ./utils/header,
./vm/interpreter, ./utils/addresses
./vm/interpreter, ./vm/interpreter/gas_costs, ./utils/addresses
func intrinsicGas*(data: openarray[byte]): GasInt =
result = 21_000
@ -33,7 +33,7 @@ proc validateTransaction*(vmState: BaseVMState, transaction: Transaction, sender
transaction.accountNonce == readOnlyDB.getNonce(sender) and
readOnlyDB.getBalance(sender) >= gas_cost
proc setupComputation*(header: BlockHeader, vmState: BaseVMState, transaction: Transaction, sender: EthAddress) : BaseComputation =
proc setupComputation*(header: BlockHeader, vmState: BaseVMState, transaction: Transaction, sender: EthAddress, forkOverride=none(Fork)) : BaseComputation =
let message = newMessage(
gas = transaction.gasLimit - transaction.payload.intrinsicGas,
gasPrice = transaction.gasPrice,
@ -45,7 +45,7 @@ proc setupComputation*(header: BlockHeader, vmState: BaseVMState, transaction: T
options = newMessageOptions(origin = sender,
createAddress = transaction.to))
result = newBaseComputation(vmState, header.blockNumber, message)
result = newBaseComputation(vmState, header.blockNumber, message, forkOverride)
doAssert result.isOriginComputation
proc execComputation*(computation: var BaseComputation): bool =
@ -59,19 +59,24 @@ proc execComputation*(computation: var BaseComputation): bool =
except ValueError:
result = false
proc applyCreateTransaction*(db: var AccountStateDB, t: Transaction, vmState: BaseVMState, sender: EthAddress, useHomestead: bool = false): UInt256 =
proc applyCreateTransaction*(db: var AccountStateDB, t: Transaction, vmState: BaseVMState, sender: EthAddress, forkOverride=none(Fork)): UInt256 =
doAssert t.isContractCreation
# TODO: clean up params
trace "Contract creation"
let gasUsed = t.payload.intrinsicGas.GasInt + (if useHomestead: 32000 else: 0)
let fork =
if forkOverride.isSome:
forkOverride.get
else:
vmState.blockNumber.toFork
let gasUsed = t.payload.intrinsicGas.GasInt + gasFees[fork][GasTXCreate]
# TODO: setupComputation refactoring
let contractAddress = generateAddress(sender, t.accountNonce)
let msg = newMessage(t.gasLimit - gasUsed, t.gasPrice, t.to, sender, t.value, @[], t.payload,
options = newMessageOptions(origin = sender,
createAddress = contractAddress))
var c = newBaseComputation(vmState, vmState.blockNumber, msg)
var c = newBaseComputation(vmState, vmState.blockNumber, msg, forkOverride)
if execComputation(c):
db.addBalance(contractAddress, t.value)

View File

@ -6,10 +6,10 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
tables, eth_common,
tables, eth_common, options,
./constants, json,
./vm/[memory, stack, code_stream],
./vm/interpreter/[gas_costs, opcode_values], # TODO - will be hidden at a lower layer
./vm/interpreter/[gas_costs, opcode_values, vm_forks], # TODO - will be hidden at a lower layer
./db/db_chain
type
@ -60,6 +60,7 @@ type
gasCosts*: GasCosts # TODO - will be hidden at a lower layer
opCodeExec*: OpcodeExecutor
lastOpCodeHasRetVal*: bool
forkOverride*: Option[Fork]
Error* = ref object
info*: string

View File

@ -505,5 +505,52 @@ func allowedFailingGeneralStateTest*(folder, name: string): bool =
"pointMulAdd.json",
"pointMulAdd2.json",
"Call1024BalanceTooLow.json",
"ExtCodeCopyTests.json"]
name in allowedFailingGeneralStateTests
"ExtCodeCopyTests.json",
# 2018-12-07:
"delegatecallAndOOGatTxLevel.json",
"delegatecallInInitcodeToExistingContractOOG.json",
"RawCallCodeGasAsk.json",
"RawCallCodeGasMemoryAsk.json",
"RawCallGasAsk.json",
"RawCallMemoryGasAsk.json",
"RawDelegateCallGasAsk.json",
"RawDelegateCallGasMemoryAsk.json",
"CallAndCallcodeConsumeMoreGasThenTransactionHasWithMemExpandingCalls.json",
"CallGoesOOGOnSecondLevel2WithMemExpandingCalls.json",
"CallGoesOOGOnSecondLevelWithMemExpandingCalls.json",
"DelegateCallOnEIPWithMemExpandingCalls.json",
"ExecuteCallThatAskMoreGasThenTransactionHasWithMemExpandingCalls.json",
"RETURN_Bounds.json",
"NonZeroValue_DELEGATECALL.json",
"NonZeroValue_DELEGATECALL_ToEmpty.json",
"NonZeroValue_DELEGATECALL_ToNonNonZeroBalance.json",
"NonZeroValue_DELEGATECALL_ToOneStorageKey.json",
"call_outsize_then_create_successful_then_returndatasize.json",
"call_then_create_successful_then_returndatasize.json",
"create_callprecompile_returndatasize.json",
"returndatacopy_0_0_following_successful_create.json",
"returndatacopy_following_create.json",
"returndatacopy_following_revert_in_create.json",
"returndatacopy_following_successful_create.json",
"returndatasize_following_successful_create.json",
"LoopCallsDepthThenRevert.json",
"LoopDelegateCallsDepthThenRevert.json",
"RevertDepth2.json",
"RevertOpcodeInCreateReturns.json",
"CallRecursiveBomb3.json",
"doubleSelfdestructTest.json",
"doubleSelfdestructTest2.json",
"ZeroValue_CALLCODE.json",
"ZeroValue_CALLCODE_ToEmpty.json",
"ZeroValue_CALLCODE_ToNonZeroBalance.json",
"ZeroValue_CALLCODE_ToOneStorageKey.json",
"ZeroValue_CALL_ToEmpty.json",
"ZeroValue_CALL_ToNonZeroBalance.json",
"ZeroValue_CALL_ToOneStorageKey.json",
"ZeroValue_DELEGATECALL.json",
"ZeroValue_DELEGATECALL_ToEmpty.json",
"ZeroValue_DELEGATECALL_ToNonZeroBalance.json",
"ZeroValue_DELEGATECALL_ToOneStorageKey.json",
]
result = name in allowedFailingGeneralStateTests

View File

@ -7,7 +7,7 @@
import
unittest, strformat, strutils, tables, json, ospaths, times,
byteutils, ranges/typedranges, nimcrypto/[keccak, hash],
byteutils, ranges/typedranges, nimcrypto/[keccak, hash], options,
rlp, eth_trie/db, eth_common,
eth_keys,
./test_helpers,
@ -23,14 +23,15 @@ suite "generalstate json tests":
jsonTest("GeneralStateTests", testFixture)
proc testFixtureIndexes(header: BlockHeader, pre: JsonNode, transaction: Transaction, sender: EthAddress, expectedHash: string) =
proc testFixtureIndexes(header: BlockHeader, pre: JsonNode, transaction: Transaction, sender: EthAddress, expectedHash: string, testStatusIMPL: var TestStatus, fork: Fork) =
var vmState = newBaseVMState(header, newBaseChainDB(newMemoryDb()))
vmState.mutateStateDB:
setupStateDB(pre, db)
defer:
#echo vmState.readOnlyStateDB.dumpAccount("c94f5374fce5edbc8e2a8697c15331677e6ebf0b")
doAssert "0x" & `$`(vmState.readOnlyStateDB.rootHash).toLowerAscii == expectedHash
let obtainedHash = "0x" & `$`(vmState.readOnlyStateDB.rootHash).toLowerAscii
check obtainedHash == expectedHash
if not validateTransaction(vmState, transaction, sender):
vmState.mutateStateDB:
@ -56,10 +57,10 @@ proc testFixtureIndexes(header: BlockHeader, pre: JsonNode, transaction: Transac
# fixtures/GeneralStateTests/stTransactionTest/TransactionSendingToEmpty.json
#db.addBalance(generateAddress(sender, transaction.accountNonce), transaction.value)
let createGasUsed = applyCreateTransaction(db, transaction, vmState, sender, true)
let createGasUsed = applyCreateTransaction(db, transaction, vmState, sender, some(fork))
db.addBalance(header.coinbase, createGasUsed)
return
var computation = setupComputation(header, vmState, transaction, sender)
var computation = setupComputation(header, vmState, transaction, sender, some(fork))
vmState.mutateStateDB:
# contract creation transaction.to == 0, so ensure happens after
@ -108,7 +109,10 @@ proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) =
)
let ftrans = fixture["transaction"]
for expectation in fixture["post"]["Homestead"]:
for fork in supportedForks:
if fixture["post"].has_key(forkNames[fork]):
# echo "[fork: ", forkNames[fork], "]"
for expectation in fixture["post"][forkNames[fork]]:
let
expectedHash = expectation["hash"].getStr
indexes = expectation["indexes"]
@ -117,4 +121,4 @@ proc testFixture(fixtures: JsonNode, testStatusIMPL: var TestStatus) =
valueIndex = indexes["value"].getInt
let transaction = ftrans.getFixtureTransaction(dataIndex, gasIndex, valueIndex)
let sender = ftrans.getFixtureTransactionSender
testFixtureIndexes(header, fixture["pre"], transaction, sender, expectedHash)
testFixtureIndexes(header, fixture["pre"], transaction, sender, expectedHash, testStatusIMPL, fork)

View File

@ -14,12 +14,25 @@ import
../nimbus/vm/interpreter/[gas_costs, vm_forks],
../tests/test_generalstate_failing
const
# from https://ethereum-tests.readthedocs.io/en/latest/test_types/state_tests.html
forkNames* = {
FkFrontier: "Frontier",
FkHomestead: "Homestead",
FkTangerine: "EIP150",
FkSpurious: "EIP158",
FkByzantium: "Byzantium",
}.toTable
supportedForks* = [FkHomestead]
type
Status* {.pure.} = enum OK, Fail, Skip
func slowTest*(folder: string, name: string): bool =
# TODO: add vmPerformance and loop check here
result = folder == "stQuadraticComplexityTest" or
result =
(folder == "vmPerformance" and "loop" in name) or
folder == "stQuadraticComplexityTest" or
name in @["randomStatetest352.json", "randomStatetest1.json",
"randomStatetest32.json", "randomStatetest347.json",
"randomStatetest393.json", "randomStatetest626.json",
@ -99,26 +112,29 @@ func allowedFailInCurrentBuild(folder, name: string): bool =
return allowedFailingGeneralStateTest(folder, name)
func validTest*(folder: string, name: string): bool =
# tests we want to skip or which segfault will be skipped here
result = (folder != "vmPerformance" or "loop" notin name) and
not slowTest(folder, name)
# we skip tests that are slow or expected to fail for now
result =
not slowTest(folder, name) and
not allowedFailInCurrentBuild(folder, name)
proc lacksHomesteadPostStates*(filename: string): bool =
proc lacksSupportedForks*(filename: string): bool =
# XXX: Until Nimbus supports Byzantine or newer forks, as opposed
# to Homestead, ~1k of ~2.5k GeneralStateTests won't work. Nimbus
# supporting Byzantine should trigger removal of this function. A
# possible alternate approach of avoiding double-reading fixtures
# seemed less than ideal, as by the time that happens, output has
# already appeared. Compatible with non-GST fixtures. Will become
# expensive once BlockchainTests appear, so try to remove first.
# to Homestead, ~1k of ~2.5k GeneralStateTests won't work.
let fixtures = parseJSON(readFile(filename))
var fixture: JsonNode
for label, child in fixtures:
fixture = child
break
return fixture.kind == JObject and fixture.has_key("transaction") and
(fixture.has_key("post") and not fixture["post"].has_key("Homestead"))
# not all fixtures make a distinction between forks, so default to accepting
# them all, until we find the ones that specify forks in their "post" section
result = false
if fixture.kind == JObject and fixture.has_key("transaction") and fixture.has_key("post"):
result = true
for fork in supportedForks:
if fixture["post"].has_key(forkNames[fork]):
result = false
break
macro jsonTest*(s: static[string], handler: untyped): untyped =
let
@ -128,30 +144,29 @@ macro jsonTest*(s: static[string], handler: untyped): untyped =
final = newIdentNode"final"
name = newIdentNode"name"
formatted = newStrLitNode"{symbol[final]} {name:<64}{$final}{'\n'}"
result = quote:
var filenames: seq[(string, string, string)] = @[]
var status = initOrderedTable[string, OrderedTable[string, Status]]()
for filename in walkDirRec("tests" / "fixtures" / `s`):
if not filename.endsWith(".json"):
continue
var (folder, name) = filename.splitPath()
let last = folder.splitPath().tail
if not status.hasKey(last):
status[last] = initOrderedTable[string, Status]()
status[last][name] = Status.Skip
if last.validTest(name) and not filename.lacksHomesteadPostStates:
if last.validTest(name) and not filename.lacksSupportedForks:
filenames.add((filename, last, name))
for child in filenames:
let (filename, folder, name) = child
# we set this here because exceptions might be raised in the handler:
status[folder][name] = Status.Fail
test filename:
echo folder / name
status[folder][name] = Status.FAIL
try:
`handler`(parseJSON(readFile(filename)), `testStatusIMPL`)
if `testStatusIMPL` == OK:
status[folder][name] = Status.OK
except AssertionError:
status[folder][name] = Status.FAIL
if not allowedFailInCurrentBuild(folder, name):
raise
status.sort do (a: (string, OrderedTable[string, Status]),
b: (string, OrderedTable[string, Status])) -> int: cmp(a[0], b[0])