Commit eabb5dd8 authored by Kelvin Fichter's avatar Kelvin Fichter

Lots of library cleanup

parent aa67fe22
...@@ -3,8 +3,9 @@ pragma solidity ^0.7.0; ...@@ -3,8 +3,9 @@ pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2; pragma experimental ABIEncoderV2;
/* Library Imports */ /* Library Imports */
import { Lib_OVMCodec } from "../../libraries/codec/Lib_OVMCodec.sol";
import { Lib_AddressResolver } from "../../libraries/resolver/Lib_AddressResolver.sol"; import { Lib_AddressResolver } from "../../libraries/resolver/Lib_AddressResolver.sol";
import { Lib_EthMerkleTrie } from "../../libraries/trie/Lib_EthMerkleTrie.sol"; import { Lib_SecureMerkleTrie } from "../../libraries/trie/Lib_SecureMerkleTrie.sol";
import { Lib_BytesUtils } from "../../libraries/utils/Lib_BytesUtils.sol"; import { Lib_BytesUtils } from "../../libraries/utils/Lib_BytesUtils.sol";
/* Interface Imports */ /* Interface Imports */
...@@ -221,11 +222,27 @@ contract OVM_L1CrossDomainMessenger is iOVM_L1CrossDomainMessenger, OVM_BaseCros ...@@ -221,11 +222,27 @@ contract OVM_L1CrossDomainMessenger is iOVM_L1CrossDomainMessenger, OVM_BaseCros
) )
); );
return Lib_EthMerkleTrie.proveAccountStorageSlotValue( (
0x4200000000000000000000000000000000000000, bool exists,
storageKey, bytes memory encodedMessagePassingAccount
bytes32(uint256(1)), ) = Lib_SecureMerkleTrie.get(
abi.encodePacked(0x4200000000000000000000000000000000000000),
_proof.stateTrieWitness, _proof.stateTrieWitness,
_proof.stateRoot
);
require(
exists == true,
"Message passing precompile has not been initialized or invalid proof provided."
);
Lib_OVMCodec.EVMAccount memory account = Lib_OVMCodec.decodeEVMAccount(
encodedMessagePassingAccount
);
return Lib_SecureMerkleTrie.verifyInclusionProof(
abi.encodePacked(storageKey),
abi.encodePacked(uint256(1)),
_proof.storageTrieWitness, _proof.storageTrieWitness,
_proof.stateRoot _proof.stateRoot
); );
......
...@@ -1519,10 +1519,12 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver { ...@@ -1519,10 +1519,12 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
bool _valid bool _valid
) )
{ {
// Always have to be below the maximum gas limit.
if (_gasLimit > gasMeterConfig.maxTransactionGasLimit) { if (_gasLimit > gasMeterConfig.maxTransactionGasLimit) {
return false; return false;
} }
// Always have to be above the minumum gas limit.
if (_gasLimit < gasMeterConfig.minTransactionGasLimit) { if (_gasLimit < gasMeterConfig.minTransactionGasLimit) {
return false; return false;
} }
......
...@@ -111,6 +111,20 @@ contract OVM_StateManager is iOVM_StateManager { ...@@ -111,6 +111,20 @@ contract OVM_StateManager is iOVM_StateManager {
accounts[_address] = _account; accounts[_address] = _account;
} }
/**
* Marks an account as empty.
* @param _address Address of the account to mark.
*/
function putEmptyAccount(
address _address
)
override
public
authenticated
{
accounts[_address].codeHash = EMPTY_ACCOUNT_CODE_HASH;
}
/** /**
* Retrieves an account from the state. * Retrieves an account from the state.
* @param _address Address of the account to retrieve. * @param _address Address of the account to retrieve.
...@@ -215,6 +229,24 @@ contract OVM_StateManager is iOVM_StateManager { ...@@ -215,6 +229,24 @@ contract OVM_StateManager is iOVM_StateManager {
return accounts[_address].ethAddress; return accounts[_address].ethAddress;
} }
/**
* Retrieves the storage root of an account.
* @param _address Address of the account to access.
* @return _storageRoot Corresponding storage root.
*/
function getAccountStorageRoot(
address _address
)
override
public
view
returns (
bytes32 _storageRoot
)
{
return accounts[_address].storageRoot;
}
/** /**
* Initializes a pending account (during CREATE or CREATE2) with the default values. * Initializes a pending account (during CREATE or CREATE2) with the default values.
* @param _address Address of the account to initialize. * @param _address Address of the account to initialize.
...@@ -228,7 +260,7 @@ contract OVM_StateManager is iOVM_StateManager { ...@@ -228,7 +260,7 @@ contract OVM_StateManager is iOVM_StateManager {
{ {
Lib_OVMCodec.Account storage account = accounts[_address]; Lib_OVMCodec.Account storage account = accounts[_address];
account.nonce = 1; account.nonce = 1;
account.codeHash = keccak256(hex'80'); account.codeHash = keccak256(hex'');
account.isFresh = true; account.isFresh = true;
} }
......
...@@ -6,7 +6,7 @@ pragma experimental ABIEncoderV2; ...@@ -6,7 +6,7 @@ pragma experimental ABIEncoderV2;
import { Lib_OVMCodec } from "../../libraries/codec/Lib_OVMCodec.sol"; import { Lib_OVMCodec } from "../../libraries/codec/Lib_OVMCodec.sol";
import { Lib_AddressResolver } from "../../libraries/resolver/Lib_AddressResolver.sol"; import { Lib_AddressResolver } from "../../libraries/resolver/Lib_AddressResolver.sol";
import { Lib_EthUtils } from "../../libraries/utils/Lib_EthUtils.sol"; import { Lib_EthUtils } from "../../libraries/utils/Lib_EthUtils.sol";
import { Lib_EthMerkleTrie } from "../../libraries/trie/Lib_EthMerkleTrie.sol"; import { Lib_SecureMerkleTrie } from "../../libraries/trie/Lib_SecureMerkleTrie.sol";
/* Interface Imports */ /* Interface Imports */
import { iOVM_StateTransitioner } from "../../iOVM/verification/iOVM_StateTransitioner.sol"; import { iOVM_StateTransitioner } from "../../iOVM/verification/iOVM_StateTransitioner.sol";
...@@ -14,6 +14,9 @@ import { iOVM_ExecutionManager } from "../../iOVM/execution/iOVM_ExecutionManage ...@@ -14,6 +14,9 @@ import { iOVM_ExecutionManager } from "../../iOVM/execution/iOVM_ExecutionManage
import { iOVM_StateManager } from "../../iOVM/execution/iOVM_StateManager.sol"; import { iOVM_StateManager } from "../../iOVM/execution/iOVM_StateManager.sol";
import { iOVM_StateManagerFactory } from "../../iOVM/execution/iOVM_StateManagerFactory.sol"; import { iOVM_StateManagerFactory } from "../../iOVM/execution/iOVM_StateManagerFactory.sol";
/* Logging Imports */
import { console } from "@nomiclabs/buidler/console.sol";
/** /**
* @title OVM_StateTransitioner * @title OVM_StateTransitioner
*/ */
...@@ -158,73 +161,115 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver { ...@@ -158,73 +161,115 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver {
*/ */
function proveContractState( function proveContractState(
address _ovmContractAddress, address _ovmContractAddress,
Lib_OVMCodec.Account memory _account, address _ethContractAddress,
Lib_OVMCodec.EVMAccount memory _account,
bytes memory _stateTrieWitness bytes memory _stateTrieWitness
) )
override override
public public
onlyDuringPhase(TransitionPhase.PRE_EXECUTION) onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
{ {
// Exit quickly to avoid unnecessary work.
require(
ovmStateManager.hasAccount(_ovmContractAddress) == false,
"Account state has already been proven"
);
require( require(
_account.codeHash == Lib_EthUtils.getCodeHash(_account.ethAddress), _account.codeHash == Lib_EthUtils.getCodeHash(_ethContractAddress),
"Invalid code hash provided." "Invalid code hash provided."
); );
require( require(
Lib_EthMerkleTrie.proveAccountState( Lib_SecureMerkleTrie.verifyInclusionProof(
_ovmContractAddress, abi.encodePacked(_ovmContractAddress),
Lib_OVMCodec.EVMAccount({ Lib_OVMCodec.encodeEVMAccount(_account),
balance: _account.balance,
nonce: _account.nonce,
storageRoot: _account.storageRoot,
codeHash: _account.codeHash
}),
_stateTrieWitness, _stateTrieWitness,
preStateRoot preStateRoot
), ),
"Invalid account state provided." "Account state is not correct or invalid inclusion proof provided."
); );
ovmStateManager.putAccount( ovmStateManager.putAccount(
_ovmContractAddress, _ovmContractAddress,
_account Lib_OVMCodec.Account({
nonce: _account.nonce,
balance: _account.balance,
storageRoot: _account.storageRoot,
codeHash: _account.codeHash,
ethAddress: _ethContractAddress,
isFresh: false
})
); );
} }
/**
* Allows a user to prove that an account does *not* exist in the state.
* @param _ovmContractAddress Address of the contract on the OVM.
* @param _stateTrieWitness Proof of the (empty) account state.
*/
function proveEmptyContractState(
address _ovmContractAddress,
bytes memory _stateTrieWitness
)
override
public
onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
{
// Exit quickly to avoid unnecessary work.
require(
ovmStateManager.hasEmptyAccount(_ovmContractAddress) == false,
"Account state has already been proven."
);
require(
Lib_SecureMerkleTrie.verifyExclusionProof(
abi.encodePacked(_ovmContractAddress),
_stateTrieWitness,
preStateRoot
),
"Account is not empty or invalid inclusion proof provided."
);
ovmStateManager.putEmptyAccount(_ovmContractAddress);
}
/** /**
* Allows a user to prove the initial state of a contract storage slot. * Allows a user to prove the initial state of a contract storage slot.
* @param _ovmContractAddress Address of the contract on the OVM. * @param _ovmContractAddress Address of the contract on the OVM.
* @param _key Claimed account slot key. * @param _key Claimed account slot key.
* @param _value Claimed account slot value. * @param _value Claimed account slot value.
* @param _stateTrieWitness Proof of the account state.
* @param _storageTrieWitness Proof of the storage slot. * @param _storageTrieWitness Proof of the storage slot.
*/ */
function proveStorageSlot( function proveStorageSlot(
address _ovmContractAddress, address _ovmContractAddress,
bytes32 _key, bytes32 _key,
bytes32 _value, bytes32 _value,
bytes memory _stateTrieWitness,
bytes memory _storageTrieWitness bytes memory _storageTrieWitness
) )
override override
public public
onlyDuringPhase(TransitionPhase.PRE_EXECUTION) onlyDuringPhase(TransitionPhase.PRE_EXECUTION)
{ {
// Exit quickly to avoid unnecessary work.
require(
ovmStateManager.hasContractStorage(_ovmContractAddress, _key) == false,
"Storage slot has already been proven."
);
require( require(
ovmStateManager.hasAccount(_ovmContractAddress) == true, ovmStateManager.hasAccount(_ovmContractAddress) == true,
"Contract must be verified before proving a storage slot." "Contract must be verified before proving a storage slot."
); );
require( require(
Lib_EthMerkleTrie.proveAccountStorageSlotValue( Lib_SecureMerkleTrie.verifyInclusionProof(
_ovmContractAddress, abi.encodePacked(_key),
_key, abi.encodePacked(_value),
_value,
_stateTrieWitness,
_storageTrieWitness, _storageTrieWitness,
preStateRoot ovmStateManager.getAccountStorageRoot(_ovmContractAddress)
), ),
"Invalid account state provided." "Storage slot is invalid or invalid inclusion proof provided."
); );
ovmStateManager.putContractStorage( ovmStateManager.putContractStorage(
...@@ -254,9 +299,14 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver { ...@@ -254,9 +299,14 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver {
"Invalid transaction provided." "Invalid transaction provided."
); );
// TODO: Set state manager for EM here. // We call `setExecutionManager` right before `run` (and not earlier) just in case the
// OVM_ExecutionManager address was updated between the time when this contract was created
// and when `applyTransaction` was called.
ovmStateManager.setExecutionManager(resolve("OVM_ExecutionManager")); ovmStateManager.setExecutionManager(resolve("OVM_ExecutionManager"));
// `run` always succeeds *unless* the user hasn't provided enough gas to `applyTransaction`
// or an INVALID_STATE_ACCESS flag was triggered. Either way, we won't get beyond this line
// if that's the case.
ovmExecutionManager.run(_transaction, address(ovmStateManager)); ovmExecutionManager.run(_transaction, address(ovmStateManager));
phase = TransitionPhase.POST_EXECUTION; phase = TransitionPhase.POST_EXECUTION;
...@@ -275,7 +325,7 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver { ...@@ -275,7 +325,7 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver {
*/ */
function commitContractState( function commitContractState(
address _ovmContractAddress, address _ovmContractAddress,
Lib_OVMCodec.Account memory _account, Lib_OVMCodec.EVMAccount memory _account,
bytes memory _stateTrieWitness bytes memory _stateTrieWitness
) )
override override
...@@ -284,17 +334,12 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver { ...@@ -284,17 +334,12 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver {
{ {
require( require(
ovmStateManager.commitAccount(_ovmContractAddress) == true, ovmStateManager.commitAccount(_ovmContractAddress) == true,
"Cannot commit an account that has not been changed." "Account was not changed or has already been committed."
); );
postStateRoot = Lib_EthMerkleTrie.updateAccountState( postStateRoot = Lib_SecureMerkleTrie.update(
_ovmContractAddress, abi.encodePacked(_ovmContractAddress),
Lib_OVMCodec.EVMAccount({ Lib_OVMCodec.encodeEVMAccount(_account),
balance: _account.balance,
nonce: _account.nonce,
storageRoot: _account.storageRoot,
codeHash: _account.codeHash
}),
_stateTrieWitness, _stateTrieWitness,
postStateRoot postStateRoot
); );
...@@ -321,15 +366,24 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver { ...@@ -321,15 +366,24 @@ contract OVM_StateTransitioner is iOVM_StateTransitioner, Lib_AddressResolver {
{ {
require( require(
ovmStateManager.commitContractStorage(_ovmContractAddress, _key) == true, ovmStateManager.commitContractStorage(_ovmContractAddress, _key) == true,
"Cannot commit a storage slot that has not been changed." "Storage slot was not changed or has already been committed."
); );
postStateRoot = Lib_EthMerkleTrie.updateAccountStorageSlotValue( Lib_OVMCodec.EVMAccount memory account = Lib_OVMCodec.toEVMAccount(
_ovmContractAddress, ovmStateManager.getAccount(_ovmContractAddress)
_key, );
_value,
_stateTrieWitness, account.storageRoot = Lib_SecureMerkleTrie.update(
abi.encodePacked(_key),
abi.encodePacked(_value),
_storageTrieWitness, _storageTrieWitness,
account.storageRoot
);
postStateRoot = Lib_SecureMerkleTrie.update(
abi.encodePacked(_ovmContractAddress),
Lib_OVMCodec.encodeEVMAccount(account),
_stateTrieWitness,
postStateRoot postStateRoot
); );
} }
......
...@@ -34,12 +34,14 @@ interface iOVM_StateManager { ...@@ -34,12 +34,14 @@ interface iOVM_StateManager {
************************************/ ************************************/
function putAccount(address _address, Lib_OVMCodec.Account memory _account) external; function putAccount(address _address, Lib_OVMCodec.Account memory _account) external;
function putEmptyAccount(address _address) external;
function getAccount(address _address) external view returns (Lib_OVMCodec.Account memory _account); function getAccount(address _address) external view returns (Lib_OVMCodec.Account memory _account);
function hasAccount(address _address) external view returns (bool _exists); function hasAccount(address _address) external view returns (bool _exists);
function hasEmptyAccount(address _address) external view returns (bool _exists); function hasEmptyAccount(address _address) external view returns (bool _exists);
function setAccountNonce(address _address, uint256 _nonce) external; function setAccountNonce(address _address, uint256 _nonce) external;
function getAccountNonce(address _address) external view returns (uint256 _nonce); function getAccountNonce(address _address) external view returns (uint256 _nonce);
function getAccountEthAddress(address _address) external view returns (address _ethAddress); function getAccountEthAddress(address _address) external view returns (address _ethAddress);
function getAccountStorageRoot(address _address) external view returns (bytes32 _storageRoot);
function initPendingAccount(address _address) external; function initPendingAccount(address _address) external;
function commitPendingAccount(address _address, address _ethAddress, bytes32 _codeHash) external; function commitPendingAccount(address _address, address _ethAddress, bytes32 _codeHash) external;
function testAndSetAccountLoaded(address _address) external returns (bool _wasAccountAlreadyLoaded); function testAndSetAccountLoaded(address _address) external returns (bool _wasAccountAlreadyLoaded);
......
...@@ -25,7 +25,13 @@ interface iOVM_StateTransitioner { ...@@ -25,7 +25,13 @@ interface iOVM_StateTransitioner {
function proveContractState( function proveContractState(
address _ovmContractAddress, address _ovmContractAddress,
Lib_OVMCodec.Account calldata _account, address _ethContractAddress,
Lib_OVMCodec.EVMAccount calldata _account,
bytes calldata _stateTrieWitness
) external;
function proveEmptyContractState(
address _ovmContractAddress,
bytes calldata _stateTrieWitness bytes calldata _stateTrieWitness
) external; ) external;
...@@ -33,7 +39,6 @@ interface iOVM_StateTransitioner { ...@@ -33,7 +39,6 @@ interface iOVM_StateTransitioner {
address _ovmContractAddress, address _ovmContractAddress,
bytes32 _key, bytes32 _key,
bytes32 _value, bytes32 _value,
bytes calldata _stateTrieWitness,
bytes calldata _storageTrieWitness bytes calldata _storageTrieWitness
) external; ) external;
...@@ -53,7 +58,7 @@ interface iOVM_StateTransitioner { ...@@ -53,7 +58,7 @@ interface iOVM_StateTransitioner {
function commitContractState( function commitContractState(
address _ovmContractAddress, address _ovmContractAddress,
Lib_OVMCodec.Account calldata _account, Lib_OVMCodec.EVMAccount calldata _account,
bytes calldata _stateTrieWitness bytes calldata _stateTrieWitness
) external; ) external;
......
...@@ -4,12 +4,24 @@ pragma experimental ABIEncoderV2; ...@@ -4,12 +4,24 @@ pragma experimental ABIEncoderV2;
/* Library Imports */ /* Library Imports */
import { Lib_RLPReader } from "../rlp/Lib_RLPReader.sol"; import { Lib_RLPReader } from "../rlp/Lib_RLPReader.sol";
import { Lib_RLPWriter } from "../rlp/Lib_RLPWriter.sol";
/** /**
* @title Lib_OVMCodec * @title Lib_OVMCodec
*/ */
library Lib_OVMCodec { library Lib_OVMCodec {
/*************
* Constants *
*************/
bytes constant internal RLP_NULL_BYTES = hex'80';
bytes constant internal NULL_BYTES = bytes('');
bytes32 constant internal NULL_BYTES32 = bytes32('');
bytes32 constant internal KECCAK256_RLP_NULL_BYTES = keccak256(RLP_NULL_BYTES);
bytes32 constant internal KECCAK256_NULL_BYTES = keccak256(NULL_BYTES);
/********* /*********
* Enums * * Enums *
*********/ *********/
...@@ -100,13 +112,13 @@ library Lib_OVMCodec { ...@@ -100,13 +112,13 @@ library Lib_OVMCodec {
EOATransaction memory _decoded EOATransaction memory _decoded
) )
{ {
Lib_RLPReader.RLPItem[] memory decoded = Lib_RLPReader.toList(Lib_RLPReader.toRlpItem(_transaction)); Lib_RLPReader.RLPItem[] memory decoded = Lib_RLPReader.readList(_transaction);
return EOATransaction({ return EOATransaction({
nonce: Lib_RLPReader.toUint(decoded[0]), nonce: Lib_RLPReader.readUint256(decoded[0]),
gasLimit: Lib_RLPReader.toUint(decoded[2]), gasLimit: Lib_RLPReader.readUint256(decoded[2]),
target: Lib_RLPReader.toAddress(decoded[3]), target: Lib_RLPReader.readAddress(decoded[3]),
data: Lib_RLPReader.toBytes(decoded[5]) data: Lib_RLPReader.readBytes(decoded[5])
}); });
} }
...@@ -151,4 +163,77 @@ library Lib_OVMCodec { ...@@ -151,4 +163,77 @@ library Lib_OVMCodec {
{ {
return keccak256(encodeTransaction(_transaction)); return keccak256(encodeTransaction(_transaction));
} }
/**
* Converts an OVM account to an EVM account.
* @param _in OVM account to convert.
* @return _out Converted EVM account.
*/
function toEVMAccount(
Account memory _in
)
internal
pure
returns (
EVMAccount memory _out
)
{
return EVMAccount({
nonce: _in.nonce,
balance: _in.balance,
storageRoot: _in.storageRoot,
codeHash: _in.codeHash
});
}
/**
* @notice RLP-encodes an account state struct.
* @param _account Account state struct.
* @return _encoded RLP-encoded account state.
*/
function encodeEVMAccount(
EVMAccount memory _account
)
internal
pure
returns (
bytes memory _encoded
)
{
bytes[] memory raw = new bytes[](4);
// Unfortunately we can't create this array outright because
// RLPWriter.encodeList will reject fixed-size arrays. Assigning
// index-by-index circumvents this issue.
raw[0] = Lib_RLPWriter.encodeUint(_account.nonce);
raw[1] = Lib_RLPWriter.encodeUint(_account.balance);
raw[2] = _account.storageRoot == 0 ? RLP_NULL_BYTES : Lib_RLPWriter.encodeBytes(abi.encodePacked(_account.storageRoot));
raw[3] = _account.codeHash == 0 ? RLP_NULL_BYTES : Lib_RLPWriter.encodeBytes(abi.encodePacked(_account.codeHash));
return Lib_RLPWriter.encodeList(raw);
}
/**
* @notice Decodes an RLP-encoded account state into a useful struct.
* @param _encoded RLP-encoded account state.
* @return _account Account state struct.
*/
function decodeEVMAccount(
bytes memory _encoded
)
internal
pure
returns (
EVMAccount memory _account
)
{
Lib_RLPReader.RLPItem[] memory accountState = Lib_RLPReader.readList(_encoded);
return EVMAccount({
nonce: Lib_RLPReader.readUint256(accountState[0]),
balance: Lib_RLPReader.readUint256(accountState[1]),
storageRoot: Lib_RLPReader.readBytes32(accountState[2]),
codeHash: Lib_RLPReader.readBytes32(accountState[3])
});
}
} }
...@@ -24,7 +24,7 @@ library Lib_SecureMerkleTrie { ...@@ -24,7 +24,7 @@ library Lib_SecureMerkleTrie {
* of a list of RLP-encoded nodes that make a path down to the target node. * of a list of RLP-encoded nodes that make a path down to the target node.
* @param _root Known root of the Merkle trie. Used to verify that the * @param _root Known root of the Merkle trie. Used to verify that the
* included proof is correctly constructed. * included proof is correctly constructed.
* @return `true` if the k/v pair exists in the trie, `false` otherwise. * @return _verified `true` if the k/v pair exists in the trie, `false` otherwise.
*/ */
function verifyInclusionProof( function verifyInclusionProof(
bytes memory _key, bytes memory _key,
...@@ -35,7 +35,7 @@ library Lib_SecureMerkleTrie { ...@@ -35,7 +35,7 @@ library Lib_SecureMerkleTrie {
internal internal
view view
returns ( returns (
bool bool _verified
) )
{ {
bytes memory key = _getSecureKey(_key); bytes memory key = _getSecureKey(_key);
...@@ -43,31 +43,28 @@ library Lib_SecureMerkleTrie { ...@@ -43,31 +43,28 @@ library Lib_SecureMerkleTrie {
} }
/** /**
* @notice Verifies a proof that a given key/value pair is *not* present in * @notice Verifies a proof that a given key is *not* present in
* the Merkle trie. * the Merkle trie.
* @param _key Key of the node to search for, as a hex string. * @param _key Key of the node to search for, as a hex string.
* @param _value Value of the node to search for, as a hex string.
* @param _proof Merkle trie inclusion proof for the node *nearest* the * @param _proof Merkle trie inclusion proof for the node *nearest* the
* target node. We effectively need to show that either the key exists and * target node.
* its value differs, or the key does not exist at all.
* @param _root Known root of the Merkle trie. Used to verify that the * @param _root Known root of the Merkle trie. Used to verify that the
* included proof is correctly constructed. * included proof is correctly constructed.
* @return `true` if the k/v pair is absent in the trie, `false` otherwise. * @return _verified `true` if the key is not present in the trie, `false` otherwise.
*/ */
function verifyExclusionProof( function verifyExclusionProof(
bytes memory _key, bytes memory _key,
bytes memory _value,
bytes memory _proof, bytes memory _proof,
bytes32 _root bytes32 _root
) )
internal internal
view view
returns ( returns (
bool bool _verified
) )
{ {
bytes memory key = _getSecureKey(_key); bytes memory key = _getSecureKey(_key);
return Lib_MerkleTrie.verifyExclusionProof(key, _value, _proof, _root); return Lib_MerkleTrie.verifyExclusionProof(key, _proof, _root);
} }
/** /**
...@@ -79,7 +76,7 @@ library Lib_SecureMerkleTrie { ...@@ -79,7 +76,7 @@ library Lib_SecureMerkleTrie {
* Otherwise, we need to modify the trie to handle the new k/v pair. * Otherwise, we need to modify the trie to handle the new k/v pair.
* @param _root Known root of the Merkle trie. Used to verify that the * @param _root Known root of the Merkle trie. Used to verify that the
* included proof is correctly constructed. * included proof is correctly constructed.
* @return Root hash of the newly constructed trie. * @return _updatedRoot Root hash of the newly constructed trie.
*/ */
function update( function update(
bytes memory _key, bytes memory _key,
...@@ -90,7 +87,7 @@ library Lib_SecureMerkleTrie { ...@@ -90,7 +87,7 @@ library Lib_SecureMerkleTrie {
internal internal
view view
returns ( returns (
bytes32 bytes32 _updatedRoot
) )
{ {
bytes memory key = _getSecureKey(_key); bytes memory key = _getSecureKey(_key);
...@@ -102,7 +99,8 @@ library Lib_SecureMerkleTrie { ...@@ -102,7 +99,8 @@ library Lib_SecureMerkleTrie {
* @param _key Key to search for, as hex bytes. * @param _key Key to search for, as hex bytes.
* @param _proof Merkle trie inclusion proof for the key. * @param _proof Merkle trie inclusion proof for the key.
* @param _root Known root of the Merkle trie. * @param _root Known root of the Merkle trie.
* @return Whether the node exists, value associated with the key if so. * @return _exists Whether or not the key exists.
* @return _value Value of the key if it exists.
*/ */
function get( function get(
bytes memory _key, bytes memory _key,
...@@ -112,8 +110,8 @@ library Lib_SecureMerkleTrie { ...@@ -112,8 +110,8 @@ library Lib_SecureMerkleTrie {
internal internal
view view
returns ( returns (
bool, bool _exists,
bytes memory bytes memory _value
) )
{ {
bytes memory key = _getSecureKey(_key); bytes memory key = _getSecureKey(_key);
...@@ -124,7 +122,7 @@ library Lib_SecureMerkleTrie { ...@@ -124,7 +122,7 @@ library Lib_SecureMerkleTrie {
* Computes the root hash for a trie with a single node. * Computes the root hash for a trie with a single node.
* @param _key Key for the single node. * @param _key Key for the single node.
* @param _value Value for the single node. * @param _value Value for the single node.
* @return Hash of the trie. * @return _updatedRoot Hash of the trie.
*/ */
function getSingleNodeRootHash( function getSingleNodeRootHash(
bytes memory _key, bytes memory _key,
...@@ -133,7 +131,7 @@ library Lib_SecureMerkleTrie { ...@@ -133,7 +131,7 @@ library Lib_SecureMerkleTrie {
internal internal
view view
returns ( returns (
bytes32 bytes32 _updatedRoot
) )
{ {
bytes memory key = _getSecureKey(_key); bytes memory key = _getSecureKey(_key);
...@@ -145,13 +143,18 @@ library Lib_SecureMerkleTrie { ...@@ -145,13 +143,18 @@ library Lib_SecureMerkleTrie {
* Private Functions * * Private Functions *
*********************/ *********************/
/**
* Computes the secure counterpart to a key.
* @param _key Key to get a secure key from.
* @return _secureKey Secure version of the key.
*/
function _getSecureKey( function _getSecureKey(
bytes memory _key bytes memory _key
) )
private private
pure pure
returns ( returns (
bytes memory bytes memory _secureKey
) )
{ {
return abi.encodePacked(keccak256(_key)); return abi.encodePacked(keccak256(_key));
......
...@@ -8,7 +8,7 @@ import { Lib_OVMCodec } from "../../optimistic-ethereum/libraries/codec/Lib_OVMC ...@@ -8,7 +8,7 @@ import { Lib_OVMCodec } from "../../optimistic-ethereum/libraries/codec/Lib_OVMC
/** /**
* @title TestLib_OVMCodec * @title TestLib_OVMCodec
*/ */
library TestLib_OVMCodec { contract TestLib_OVMCodec {
function decodeEOATransaction( function decodeEOATransaction(
bytes memory _transaction bytes memory _transaction
......
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/* Library Imports */
import { Lib_RLPReader } from "../../optimistic-ethereum/libraries/rlp/Lib_RLPReader.sol";
/**
* @title TestLib_RLPReader
*/
contract TestLib_RLPReader {
function readList(
bytes memory _in
)
public
view
returns (
bytes[] memory
)
{
Lib_RLPReader.RLPItem[] memory decoded = Lib_RLPReader.readList(_in);
bytes[] memory out = new bytes[](decoded.length);
for (uint256 i = 0; i < out.length; i++) {
out[i] = Lib_RLPReader.readRawBytes(decoded[i]);
}
return out;
}
function readString(
bytes memory _in
)
public
view
returns (
string memory
)
{
return Lib_RLPReader.readString(_in);
}
function readBytes(
bytes memory _in
)
public
view
returns (
bytes memory
)
{
return Lib_RLPReader.readBytes(_in);
}
function readBytes32(
bytes memory _in
)
public
view
returns (
bytes32
)
{
return Lib_RLPReader.readBytes32(_in);
}
function readUint256(
bytes memory _in
)
public
view
returns (
uint256
)
{
return Lib_RLPReader.readUint256(_in);
}
function readBool(
bytes memory _in
)
public
view
returns (
bool
)
{
return Lib_RLPReader.readBool(_in);
}
function readAddress(
bytes memory _in
)
public
view
returns (
address
)
{
return Lib_RLPReader.readAddress(_in);
}
}
...@@ -8,7 +8,7 @@ import { Lib_RLPWriter } from "../../optimistic-ethereum/libraries/rlp/Lib_RLPWr ...@@ -8,7 +8,7 @@ import { Lib_RLPWriter } from "../../optimistic-ethereum/libraries/rlp/Lib_RLPWr
/** /**
* @title TestLib_RLPWriter * @title TestLib_RLPWriter
*/ */
library TestLib_RLPWriter { contract TestLib_RLPWriter {
function encodeBytes( function encodeBytes(
bytes memory _in bytes memory _in
......
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;
/* Library Imports */
import { Lib_EthMerkleTrie } from "../../optimistic-ethereum/libraries/trie/Lib_EthMerkleTrie.sol";
import { Lib_OVMCodec } from "../../optimistic-ethereum/libraries/codec/Lib_OVMCodec.sol";
/**
* @title TestLib_EthMerkleTrie
*/
contract TestLib_EthMerkleTrie {
function proveAccountStorageSlotValue(
address _address,
bytes32 _key,
bytes32 _value,
bytes memory _stateTrieWitness,
bytes memory _storageTrieWitness,
bytes32 _stateTrieRoot
)
public
view
returns (bool)
{
return Lib_EthMerkleTrie.proveAccountStorageSlotValue(
_address,
_key,
_value,
_stateTrieWitness,
_storageTrieWitness,
_stateTrieRoot
);
}
function updateAccountStorageSlotValue(
address _address,
bytes32 _key,
bytes32 _value,
bytes memory _stateTrieWitness,
bytes memory _storageTrieWitness,
bytes32 _stateTrieRoot
)
public
view
returns (bytes32)
{
return Lib_EthMerkleTrie.updateAccountStorageSlotValue(
_address,
_key,
_value,
_stateTrieWitness,
_storageTrieWitness,
_stateTrieRoot
);
}
function proveAccountState(
address _address,
Lib_OVMCodec.EVMAccount memory _accountState,
bytes memory _stateTrieWitness,
bytes32 _stateTrieRoot
)
public
view
returns (bool)
{
return Lib_EthMerkleTrie.proveAccountState(
_address,
_accountState,
_stateTrieWitness,
_stateTrieRoot
);
}
function updateAccountState(
address _address,
Lib_OVMCodec.EVMAccount memory _accountState,
bytes memory _stateTrieWitness,
bytes32 _stateTrieRoot
)
public
view
returns (bytes32)
{
return Lib_EthMerkleTrie.updateAccountState(
_address,
_accountState,
_stateTrieWitness,
_stateTrieRoot
);
}
}
...@@ -7,7 +7,7 @@ import { Lib_MerkleTrie } from "../../optimistic-ethereum/libraries/trie/Lib_Mer ...@@ -7,7 +7,7 @@ import { Lib_MerkleTrie } from "../../optimistic-ethereum/libraries/trie/Lib_Mer
/** /**
* @title TestLib_MerkleTrie * @title TestLib_MerkleTrie
*/ */
library TestLib_MerkleTrie { contract TestLib_MerkleTrie {
function verifyInclusionProof( function verifyInclusionProof(
bytes memory _key, bytes memory _key,
...@@ -31,7 +31,6 @@ library TestLib_MerkleTrie { ...@@ -31,7 +31,6 @@ library TestLib_MerkleTrie {
function verifyExclusionProof( function verifyExclusionProof(
bytes memory _key, bytes memory _key,
bytes memory _value,
bytes memory _proof, bytes memory _proof,
bytes32 _root bytes32 _root
) )
...@@ -43,7 +42,6 @@ library TestLib_MerkleTrie { ...@@ -43,7 +42,6 @@ library TestLib_MerkleTrie {
{ {
return Lib_MerkleTrie.verifyExclusionProof( return Lib_MerkleTrie.verifyExclusionProof(
_key, _key,
_value,
_proof, _proof,
_root _root
); );
......
...@@ -8,7 +8,7 @@ import { Lib_SecureMerkleTrie } from "../../optimistic-ethereum/libraries/trie/L ...@@ -8,7 +8,7 @@ import { Lib_SecureMerkleTrie } from "../../optimistic-ethereum/libraries/trie/L
/** /**
* @title TestLib_SecureMerkleTrie * @title TestLib_SecureMerkleTrie
*/ */
library TestLib_SecureMerkleTrie { contract TestLib_SecureMerkleTrie {
function verifyInclusionProof( function verifyInclusionProof(
bytes memory _key, bytes memory _key,
...@@ -32,7 +32,6 @@ library TestLib_SecureMerkleTrie { ...@@ -32,7 +32,6 @@ library TestLib_SecureMerkleTrie {
function verifyExclusionProof( function verifyExclusionProof(
bytes memory _key, bytes memory _key,
bytes memory _value,
bytes memory _proof, bytes memory _proof,
bytes32 _root bytes32 _root
) )
...@@ -44,7 +43,6 @@ library TestLib_SecureMerkleTrie { ...@@ -44,7 +43,6 @@ library TestLib_SecureMerkleTrie {
{ {
return Lib_SecureMerkleTrie.verifyExclusionProof( return Lib_SecureMerkleTrie.verifyExclusionProof(
_key, _key,
_value,
_proof, _proof,
_root _root
); );
......
...@@ -7,7 +7,7 @@ import { Lib_Bytes32Utils } from "../../optimistic-ethereum/libraries/utils/Lib_ ...@@ -7,7 +7,7 @@ import { Lib_Bytes32Utils } from "../../optimistic-ethereum/libraries/utils/Lib_
/** /**
* @title TestLib_Byte32Utils * @title TestLib_Byte32Utils
*/ */
library TestLib_Bytes32Utils { contract TestLib_Bytes32Utils {
function toBool( function toBool(
bytes32 _in bytes32 _in
......
...@@ -8,7 +8,7 @@ import { Lib_BytesUtils } from "../../optimistic-ethereum/libraries/utils/Lib_By ...@@ -8,7 +8,7 @@ import { Lib_BytesUtils } from "../../optimistic-ethereum/libraries/utils/Lib_By
/** /**
* @title TestLib_BytesUtils * @title TestLib_BytesUtils
*/ */
library TestLib_BytesUtils { contract TestLib_BytesUtils {
function concat( function concat(
bytes memory _preBytes, bytes memory _preBytes,
......
...@@ -7,7 +7,7 @@ import { Lib_ECDSAUtils } from "../../optimistic-ethereum/libraries/utils/Lib_EC ...@@ -7,7 +7,7 @@ import { Lib_ECDSAUtils } from "../../optimistic-ethereum/libraries/utils/Lib_EC
/** /**
* @title TestLib_ECDSAUtils * @title TestLib_ECDSAUtils
*/ */
library TestLib_ECDSAUtils { contract TestLib_ECDSAUtils {
function recover( function recover(
bytes memory _message, bytes memory _message,
......
...@@ -8,7 +8,7 @@ import { Lib_EthUtils } from "../../optimistic-ethereum/libraries/utils/Lib_EthU ...@@ -8,7 +8,7 @@ import { Lib_EthUtils } from "../../optimistic-ethereum/libraries/utils/Lib_EthU
/** /**
* @title TestLib_EthUtils * @title TestLib_EthUtils
*/ */
library TestLib_EthUtils { contract TestLib_EthUtils {
function getCode( function getCode(
address _address, address _address,
......
...@@ -8,7 +8,7 @@ import { Lib_MerkleUtils } from "../../optimistic-ethereum/libraries/utils/Lib_M ...@@ -8,7 +8,7 @@ import { Lib_MerkleUtils } from "../../optimistic-ethereum/libraries/utils/Lib_M
/** /**
* @title TestLib_MerkleUtils * @title TestLib_MerkleUtils
*/ */
library TestLib_MerkleUtils { contract TestLib_MerkleUtils {
function getMerkleRoot( function getMerkleRoot(
bytes32[] memory _hashes bytes32[] memory _hashes
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
"build:dump": "ts-node \"bin/take-dump.ts\"", "build:dump": "ts-node \"bin/take-dump.ts\"",
"build:copy": "copyfiles -u 2 \"contracts/optimistic-ethereum/**/*.sol\" \"build/contracts\"", "build:copy": "copyfiles -u 2 \"contracts/optimistic-ethereum/**/*.sol\" \"build/contracts\"",
"test": "yarn run test:contracts", "test": "yarn run test:contracts",
"test:contracts": "buidler test \"test/contracts/libraries/trie/Lib_EthMerkleTrie.spec.ts\" --show-stack-traces", "test:contracts": "buidler test \"test/contracts/libraries/rlp/Lib_RLPReader.spec.ts\" --show-stack-traces",
"lint": "yarn run lint:typescript", "lint": "yarn run lint:typescript",
"lint:typescript": "tslint --format stylish --project .", "lint:typescript": "tslint --format stylish --project .",
"lint:fix": "yarn run lint:fix:typescript", "lint:fix": "yarn run lint:fix:typescript",
......
This diff is collapsed.
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
import { expect } from '../../../setup' import { expect } from '../../../setup'
/* External Imports */
import { ethers } from '@nomiclabs/buidler'
import { BigNumber, Contract, ContractFactory } from 'ethers'
/* Internal Imports */
import {
makeAddressManager,
NON_NULL_BYTES32,
NON_ZERO_ADDRESS,
NULL_BYTES32,
setProxyTarget,
TrieTestGenerator,
ZERO_ADDRESS,
} from '../../../helpers'
import { MockContract, smockit } from '@eth-optimism/smock'
import { keccak256 } from 'ethers/lib/utils'
describe('OVM_StateTransitioner', () => { describe('OVM_StateTransitioner', () => {
let AddressManager: Contract
before(async () => {
AddressManager = await makeAddressManager()
})
let Mock__OVM_ExecutionManager: MockContract
let Mock__OVM_StateManagerFactory: MockContract
let Mock__OVM_StateManager: MockContract
before(async () => {
Mock__OVM_ExecutionManager = smockit(
await ethers.getContractFactory('OVM_ExecutionManager')
)
Mock__OVM_StateManagerFactory = smockit(
await ethers.getContractFactory('OVM_StateManagerFactory')
)
Mock__OVM_StateManager = smockit(
await ethers.getContractFactory('OVM_StateManager')
)
await setProxyTarget(
AddressManager,
'OVM_ExecutionManager',
Mock__OVM_ExecutionManager
)
await setProxyTarget(
AddressManager,
'OVM_StateManagerFactory',
Mock__OVM_StateManagerFactory
)
Mock__OVM_StateManagerFactory.smocked.create.will.return.with(
Mock__OVM_StateManager.address
)
Mock__OVM_StateManager.smocked.putAccount.will.return()
})
let Factory__OVM_StateTransitioner: ContractFactory
before(async () => {
Factory__OVM_StateTransitioner = await ethers.getContractFactory(
'OVM_StateTransitioner'
)
})
let OVM_StateTransitioner: Contract
beforeEach(async () => {
OVM_StateTransitioner = await Factory__OVM_StateTransitioner.deploy(
AddressManager.address,
0,
NULL_BYTES32,
NULL_BYTES32
)
})
describe('proveContractState', () => { describe('proveContractState', () => {
let account: any
beforeEach(() => {
account = {
nonce: 0,
balance: 0,
storageRoot: NULL_BYTES32,
codeHash: NULL_BYTES32,
ethAddress: ZERO_ADDRESS,
isFresh: false,
}
})
describe('when provided an invalid code hash', () => { describe('when provided an invalid code hash', () => {
it('should revert', async () => {}) beforeEach(() => {
account.ethAddress = NON_ZERO_ADDRESS
account.codeHash = NON_NULL_BYTES32
})
it('should revert', async () => {
await expect(
OVM_StateTransitioner.proveContractState(ZERO_ADDRESS, account, '0x')
).to.be.revertedWith('Invalid code hash provided.')
})
}) })
describe('when provided a valid code hash', () => { describe('when provided a valid code hash', () => {
beforeEach(async () => {
account.ethAddress = OVM_StateTransitioner.address
account.codeHash = keccak256(
await ethers.provider.getCode(OVM_StateTransitioner.address)
)
})
describe('when provided an invalid account inclusion proof', () => { describe('when provided an invalid account inclusion proof', () => {
it('should revert', async () => {}) const proof = '0x'
it('should revert', async () => {
await expect(
OVM_StateTransitioner.proveContractState(
ZERO_ADDRESS,
account,
proof
)
).to.be.reverted
})
}) })
describe('when provided a valid account inclusion proof', () => {}) describe('when provided a valid account inclusion proof', () => {
let proof: string
beforeEach(async () => {
const generator = await TrieTestGenerator.fromAccounts({
accounts: [
{
...account,
address: NON_ZERO_ADDRESS,
},
],
secure: true,
})
const test = await generator.makeAccountProofTest(NON_ZERO_ADDRESS)
proof = test.accountTrieWitness
OVM_StateTransitioner = await Factory__OVM_StateTransitioner.deploy(
AddressManager.address,
0,
test.accountTrieRoot,
NULL_BYTES32
)
})
it('should put the account in the state manager', async () => {
await expect(
OVM_StateTransitioner.proveContractState(
NON_ZERO_ADDRESS,
account,
proof
)
).to.not.be.reverted
expect(
Mock__OVM_StateManager.smocked.putAccount.calls[0]
).to.deep.equal([
NON_ZERO_ADDRESS,
[
BigNumber.from(account.nonce),
BigNumber.from(account.balance),
account.storageRoot,
account.codeHash,
account.ethAddress,
account.isFresh,
],
])
})
})
}) })
}) })
describe('proveStorageSlot', () => { describe('proveStorageSlot', () => {
describe('when the corresponding account is not proven', () => { describe('when the corresponding account is not proven', () => {
it('should revert', async () => {}) beforeEach(() => {
Mock__OVM_StateManager.smocked.hasAccount.will.return.with(false)
})
it('should revert', async () => {
await expect(
OVM_StateTransitioner.proveStorageSlot(
NON_ZERO_ADDRESS,
NON_NULL_BYTES32,
NON_NULL_BYTES32,
'0x',
'0x'
)
).to.be.revertedWith(
'Contract must be verified before proving a storage slot.'
)
})
}) })
describe('when the corresponding account is proven', () => { describe('when the corresponding account is proven', () => {
beforeEach(() => {
Mock__OVM_StateManager.smocked.hasAccount.will.return.with(false)
})
describe('when provided an invalid slot inclusion proof', () => { describe('when provided an invalid slot inclusion proof', () => {
let account: any
let proof: string
beforeEach(async () => {
account = {
nonce: 0,
balance: 0,
storageRoot: NULL_BYTES32,
codeHash: NULL_BYTES32,
ethAddress: ZERO_ADDRESS,
isFresh: false,
}
const generator = await TrieTestGenerator.fromAccounts({
accounts: [
{
...account,
address: NON_ZERO_ADDRESS,
storage: [
{
key: keccak256('0x1234'),
val: keccak256('0x5678'),
},
],
},
],
secure: true,
})
const test = await generator.makeAccountProofTest(NON_ZERO_ADDRESS)
proof = test.accountTrieWitness
OVM_StateTransitioner = await Factory__OVM_StateTransitioner.deploy(
AddressManager.address,
0,
test.accountTrieRoot,
NULL_BYTES32
)
})
it('should revert', async () => {}) it('should revert', async () => {})
}) })
......
/* External Imports */
import * as rlp from 'rlp'
/* Internal Imports */
import { Lib_RLPReader_TEST_JSON } from '../../../data'
import { runJsonTest, toHexString } from '../../../helpers'
describe('Lib_RLPReader', () => {
//console.log(JSON.stringify(Lib_RLPReader_TEST_JSON2, null, 4))
describe('JSON tests', () => {
runJsonTest('TestLib_RLPReader', Lib_RLPReader_TEST_JSON)
})
})
export { tests as Lib_RLPWriter_TEST_JSON } from './json/libraries/rlp/Lib_RLPWriter.test.json' export { tests as Lib_RLPWriter_TEST_JSON } from './json/libraries/rlp/Lib_RLPWriter.test.json'
export { tests as Lib_RLPReader_TEST_JSON } from './json/libraries/rlp/Lib_RLPReader.test.json'
export { tests as Lib_Bytes32Utils_TEST_JSON } from './json/libraries/utils/Lib_Bytes32Utils.test.json' export { tests as Lib_Bytes32Utils_TEST_JSON } from './json/libraries/utils/Lib_Bytes32Utils.test.json'
export { tests as Lib_BytesUtils_TEST_JSON } from './json/libraries/utils/Lib_BytesUtils.test.json' export { tests as Lib_BytesUtils_TEST_JSON } from './json/libraries/utils/Lib_BytesUtils.test.json'
export { tests as Lib_ECDSAUtils_TEST_JSON } from './json/libraries/utils/Lib_ECDSAUtils.test.json' export { tests as Lib_ECDSAUtils_TEST_JSON } from './json/libraries/utils/Lib_ECDSAUtils.test.json'
......
...@@ -48,8 +48,8 @@ const rlpEncodeAccount = (account: EthereumAccount): string => { ...@@ -48,8 +48,8 @@ const rlpEncodeAccount = (account: EthereumAccount): string => {
rlp.encode([ rlp.encode([
account.nonce, account.nonce,
account.balance, account.balance,
account.codeHash || NULL_BYTES32,
account.storageRoot || NULL_BYTES32, account.storageRoot || NULL_BYTES32,
account.codeHash || NULL_BYTES32,
]) ])
) )
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment