未验证 提交 7e74df6d 编写于 作者: A arhag 提交者: GitHub

Merge pull request #4429 from EOSIO/4142-eosio-sudo-simple

Add eosio.sudo contract
......@@ -10,6 +10,7 @@ add_subdirectory(libc++)
add_subdirectory(simple.token)
add_subdirectory(eosio.token)
add_subdirectory(eosio.msig)
add_subdirectory(eosio.sudo)
add_subdirectory(multi_index_test)
add_subdirectory(eosio.system)
add_subdirectory(identity)
......
file(GLOB ABI_FILES "*.abi")
configure_file("${ABI_FILES}" "${CMAKE_CURRENT_BINARY_DIR}" COPYONLY)
add_wast_executable(TARGET eosio.sudo
INCLUDE_FOLDERS "${STANDARD_INCLUDE_FOLDERS}"
LIBRARIES libc++ libc eosiolib
DESTINATION_FOLDER ${CMAKE_CURRENT_BINARY_DIR}
)
此差异已折叠。
{
"version": "eosio::abi/1.0",
"types": [{
"new_type_name": "account_name",
"type": "name"
},{
"new_type_name": "permission_name",
"type": "name"
},{
"new_type_name": "action_name",
"type": "name"
}],
"structs": [{
"name": "permission_level",
"base": "",
"fields": [
{"name": "actor", "type": "account_name"},
{"name": "permission", "type": "permission_name"}
]
},{
"name": "action",
"base": "",
"fields": [
{"name": "account", "type": "account_name"},
{"name": "name", "type": "action_name"},
{"name": "authorization", "type": "permission_level[]"},
{"name": "data", "type": "bytes"}
]
},{
"name": "transaction_header",
"base": "",
"fields": [
{"name": "expiration", "type": "time_point_sec"},
{"name": "ref_block_num", "type": "uint16"},
{"name": "ref_block_prefix", "type": "uint32"},
{"name": "max_net_usage_words", "type": "varuint32"},
{"name": "max_cpu_usage_ms", "type": "uint8"},
{"name": "delay_sec", "type": "varuint32"}
]
},{
"name": "extension",
"base": "",
"fields": [
{"name": "type", "type" : "uint16" },
{"name": "data", "type": "bytes"}
]
},{
"name": "transaction",
"base": "transaction_header",
"fields": [
{"name": "context_free_actions", "type": "action[]"},
{"name": "actions", "type": "action[]"},
{"name": "transaction_extensions", "type": "extension[]"}
]
},{
"name": "exec",
"base": "",
"fields": [
{"name":"executer", "type":"account_name"},
{"name":"trx", "type":"transaction"}
]
}
],
"actions": [{
"name": "exec",
"type": "exec",
"ricardian_contract": ""
}
],
"tables": [],
"ricardian_clauses": [],
"abi_extensions": []
}
#include <eosio.sudo/eosio.sudo.hpp>
#include <eosiolib/transaction.hpp>
namespace eosio {
/*
exec function manually parses input data (instead of taking parsed arguments from dispatcher)
because parsing data in the dispatcher uses too much CPU if the included transaction is very big
If we use dispatcher the function signature should be:
void sudo::exec( account_name executer,
transaction trx )
*/
void sudo::exec() {
require_auth( _self );
constexpr size_t max_stack_buffer_size = 512;
size_t size = action_data_size();
char* buffer = (char*)( max_stack_buffer_size < size ? malloc(size) : alloca(size) );
read_action_data( buffer, size );
account_name executer;
datastream<const char*> ds( buffer, size );
ds >> executer;
require_auth( executer );
size_t trx_pos = ds.tellp();
send_deferred( (uint128_t(executer) << 64) | current_time(), executer, buffer+trx_pos, size-trx_pos );
}
} /// namespace eosio
EOSIO_ABI( eosio::sudo, (exec) )
#pragma once
#include <eosiolib/eosio.hpp>
namespace eosio {
class sudo : public contract {
public:
sudo( account_name self ):contract(self){}
void exec();
};
} /// namespace eosio
......@@ -123,9 +123,9 @@ namespace eosio { namespace testing {
action get_action( account_name code, action_name acttype, vector<permission_level> auths,
const variant_object& data )const;
void set_transaction_headers( transaction& trx,
uint32_t expiration = DEFAULT_EXPIRATION_DELTA,
uint32_t delay_sec = 0 )const;
void set_transaction_headers( transaction& trx,
uint32_t expiration = DEFAULT_EXPIRATION_DELTA,
uint32_t delay_sec = 0 )const;
vector<transaction_trace_ptr> create_accounts( vector<account_name> names,
bool multisig = false,
......
......@@ -1709,7 +1709,7 @@ int main( int argc, char** argv ) {
auto abi = fc::json::to_pretty_string( result["abi"] );
if( filename.size() ) {
std::cout << localized("saving abi to ${filename}", ("filename", filename)) << std::endl;
std::cerr << localized("saving abi to ${filename}", ("filename", filename)) << std::endl;
std::ofstream abiout( filename.c_str() );
abiout << abi;
} else {
......@@ -2016,23 +2016,23 @@ int main( int argc, char** argv ) {
wastPath = (cpath / (cpath.filename().generic_string()+".wast")).generic_string();
}
std::cout << localized(("Reading WAST/WASM from " + wastPath + "...").c_str()) << std::endl;
std::cerr << localized(("Reading WAST/WASM from " + wastPath + "...").c_str()) << std::endl;
fc::read_file_contents(wastPath, wast);
FC_ASSERT( !wast.empty(), "no wast file found ${f}", ("f", wastPath) );
vector<uint8_t> wasm;
const string binary_wasm_header("\x00\x61\x73\x6d", 4);
if(wast.compare(0, 4, binary_wasm_header) == 0) {
std::cout << localized("Using already assembled WASM...") << std::endl;
std::cerr << localized("Using already assembled WASM...") << std::endl;
wasm = vector<uint8_t>(wast.begin(), wast.end());
}
else {
std::cout << localized("Assembling WASM...") << std::endl;
std::cerr << localized("Assembling WASM...") << std::endl;
wasm = wast_to_wasm(wast);
}
actions.emplace_back( create_setcode(account, bytes(wasm.begin(), wasm.end()) ) );
if ( shouldSend ) {
std::cout << localized("Setting Code...") << std::endl;
std::cerr << localized("Setting Code...") << std::endl;
send_actions(std::move(actions), 10000, packed_transaction::zlib);
}
};
......@@ -2052,7 +2052,7 @@ int main( int argc, char** argv ) {
actions.emplace_back( create_setabi(account, fc::json::from_file(abiPath).as<abi_def>()) );
} EOS_RETHROW_EXCEPTIONS(abi_type_exception, "Fail to parse ABI JSON")
if ( shouldSend ) {
std::cout << localized("Setting ABI...") << std::endl;
std::cerr << localized("Setting ABI...") << std::endl;
send_actions(std::move(actions), 10000, packed_transaction::zlib);
}
};
......@@ -2064,7 +2064,7 @@ int main( int argc, char** argv ) {
shouldSend = false;
set_code_callback();
set_abi_callback();
std::cout << localized("Publishing contract...") << std::endl;
std::cerr << localized("Publishing contract...") << std::endl;
send_actions(std::move(actions), 10000, packed_transaction::zlib);
});
codeSubcommand->set_callback(set_code_callback);
......@@ -2425,8 +2425,7 @@ int main( int argc, char** argv ) {
return true;
};
auto propose_action = msig->add_subcommand("propose", localized("Propose transaction"));
//auto propose_action = msig->add_subcommand("action", localized("Propose action"));
auto propose_action = msig->add_subcommand("propose", localized("Propose action"));
add_standard_transaction_options(propose_action);
propose_action->add_option("proposal_name", proposal_name, localized("proposal name (string)"))->required();
propose_action->add_option("requested_permissions", requested_perm, localized("The JSON string or filename defining requested permissions"))->required();
......@@ -2452,13 +2451,12 @@ int main( int argc, char** argv ) {
} EOS_RETHROW_EXCEPTIONS(transaction_type_exception, "Fail to parse transaction JSON '${data}'", ("data",proposed_transaction))
transaction proposed_trx = trx_var.as<transaction>();
auto arg= fc::mutable_variant_object()
auto arg = fc::mutable_variant_object()
("code", proposed_contract)
("action", proposed_action)
("args", trx_var);
auto result = call(json_to_bin_func, arg);
//std::cout << "Result: "; fc::json::to_stream(std::cout, result); std::cout << std::endl;
bytes proposed_trx_serialized = result.get_object()["binargs"].as<bytes>();
......@@ -2521,9 +2519,53 @@ int main( int argc, char** argv ) {
}
};
//multisige propose transaction
auto propose_trx = msig->add_subcommand("propose_trx", localized("Propose transaction"));
add_standard_transaction_options(propose_trx);
propose_trx->add_option("proposal_name", proposal_name, localized("proposal name (string)"))->required();
propose_trx->add_option("requested_permissions", requested_perm, localized("The JSON string or filename defining requested permissions"))->required();
propose_trx->add_option("transaction", trx_to_push, localized("The JSON string or filename defining the transaction to push"))->required();
propose_trx->add_option("proposer", proposer, localized("Account proposing the transaction"));
propose_trx->set_callback([&] {
fc::variant requested_perm_var;
try {
requested_perm_var = json_from_file_or_string(requested_perm);
} EOS_RETHROW_EXCEPTIONS(transaction_type_exception, "Fail to parse permissions JSON '${data}'", ("data",requested_perm))
fc::variant trx_var;
try {
trx_var = json_from_file_or_string(trx_to_push);
} EOS_RETHROW_EXCEPTIONS(transaction_type_exception, "Fail to parse transaction JSON '${data}'", ("data",trx_to_push))
auto accountPermissions = get_account_permissions(tx_permission);
if (accountPermissions.empty()) {
if (!proposer.empty()) {
accountPermissions = vector<permission_level>{{proposer, config::active_name}};
} else {
EOS_THROW(missing_auth_exception, "Authority is not provided (either by multisig parameter <proposer> or -p)");
}
}
if (proposer.empty()) {
proposer = name(accountPermissions.at(0).actor).to_string();
}
auto arg = fc::mutable_variant_object()
("code", "eosio.msig")
("action", "propose")
("args", fc::mutable_variant_object()
("proposer", proposer )
("proposal_name", proposal_name)
("requested", requested_perm_var)
("trx", trx_var)
);
auto result = call(json_to_bin_func, arg);
send_actions({chain::action{accountPermissions, "eosio.msig", "propose", result.get_object()["binargs"].as<bytes>()}});
});
// multisig review
auto review = msig->add_subcommand("review", localized("Review transaction"));
add_standard_transaction_options(review);
review->add_option("proposer", proposer, localized("proposer name (string)"))->required();
review->add_option("proposal_name", proposal_name, localized("proposal name (string)"))->required();
......
#include <boost/test/unit_test.hpp>
#include <eosio/testing/tester.hpp>
#include <eosio/chain/abi_serializer.hpp>
#include <eosio/chain/wast_to_wasm.hpp>
#include <eosio.msig/eosio.msig.wast.hpp>
#include <eosio.msig/eosio.msig.abi.hpp>
#include <eosio.sudo/eosio.sudo.wast.hpp>
#include <eosio.sudo/eosio.sudo.abi.hpp>
#include <test_api/test_api.wast.hpp>
#include <Runtime/Runtime.h>
#include <fc/variant_object.hpp>
using namespace eosio::testing;
using namespace eosio;
using namespace eosio::chain;
using namespace eosio::testing;
using namespace fc;
using mvo = fc::mutable_variant_object;
class eosio_sudo_tester : public tester {
public:
eosio_sudo_tester() {
create_accounts( { N(eosio.msig), N(prod1), N(prod2), N(prod3), N(prod4), N(prod5), N(alice), N(bob), N(carol) } );
produce_block();
base_tester::push_action(config::system_account_name, N(setpriv),
config::system_account_name, mutable_variant_object()
("account", "eosio.msig")
("is_priv", 1)
);
set_code( N(eosio.msig), eosio_msig_wast );
set_abi( N(eosio.msig), eosio_msig_abi );
produce_blocks();
signed_transaction trx;
set_transaction_headers(trx);
authority auth( 1, {}, {{{config::system_account_name, config::active_name}, 1}} );
trx.actions.emplace_back( vector<permission_level>{{config::system_account_name, config::active_name}},
newaccount{
.creator = config::system_account_name,
.name = N(eosio.sudo),
.owner = auth,
.active = auth,
});
set_transaction_headers(trx);
trx.sign( get_private_key( config::system_account_name, "active" ), control->get_chain_id() );
push_transaction( trx );
base_tester::push_action(config::system_account_name, N(setpriv),
config::system_account_name, mutable_variant_object()
("account", "eosio.sudo")
("is_priv", 1)
);
auto system_private_key = get_private_key( config::system_account_name, "active" );
set_code( N(eosio.sudo), eosio_sudo_wast, &system_private_key );
set_abi( N(eosio.sudo), eosio_sudo_abi, &system_private_key );
produce_blocks();
set_authority( config::system_account_name, config::active_name,
authority( 1, {{get_public_key( config::system_account_name, "active" ), 1}},
{{{config::producers_account_name, config::active_name}, 1}} ),
config::owner_name,
{ { config::system_account_name, config::owner_name } },
{ get_private_key( config::system_account_name, "active" ) }
);
set_producers( {N(prod1), N(prod2), N(prod3), N(prod4), N(prod5)} );
produce_blocks();
const auto& accnt = control->db().get<account_object,by_name>( N(eosio.sudo) );
abi_def abi;
BOOST_REQUIRE_EQUAL(abi_serializer::to_abi(accnt.abi, abi), true);
abi_ser.set_abi(abi);
while( control->pending_block_state()->header.producer.to_string() == "eosio" ) {
produce_block();
}
}
void propose( name proposer, name proposal_name, vector<permission_level> requested_permissions, const transaction& trx ) {
push_action( N(eosio.msig), N(propose), proposer, mvo()
("proposer", proposer)
("proposal_name", proposal_name)
("requested", requested_permissions)
("trx", trx)
);
}
void approve( name proposer, name proposal_name, name approver ) {
push_action( N(eosio.msig), N(approve), approver, mvo()
("proposer", proposer)
("proposal_name", proposal_name)
("level", permission_level{approver, config::active_name} )
);
}
void unapprove( name proposer, name proposal_name, name unapprover ) {
push_action( N(eosio.msig), N(unapprove), unapprover, mvo()
("proposer", proposer)
("proposal_name", proposal_name)
("level", permission_level{unapprover, config::active_name})
);
}
transaction sudo_exec( account_name executer, const transaction& trx, uint32_t expiration = base_tester::DEFAULT_EXPIRATION_DELTA );
transaction reqauth( account_name from, const vector<permission_level>& auths, uint32_t expiration = base_tester::DEFAULT_EXPIRATION_DELTA );
abi_serializer abi_ser;
};
transaction eosio_sudo_tester::sudo_exec( account_name executer, const transaction& trx, uint32_t expiration ) {
fc::variants v;
v.push_back( fc::mutable_variant_object()
("actor", executer)
("permission", name{config::active_name})
);
v.push_back( fc::mutable_variant_object()
("actor", "eosio.sudo")
("permission", name{config::active_name})
);
auto act_obj = fc::mutable_variant_object()
("account", "eosio.sudo")
("name", "exec")
("authorization", v)
("data", fc::mutable_variant_object()("executer", executer)("trx", trx) );
transaction trx2;
set_transaction_headers(trx2, expiration);
action act;
abi_serializer::from_variant( act_obj, act, get_resolver() );
trx2.actions.push_back( std::move(act) );
return trx2;
}
transaction eosio_sudo_tester::reqauth( account_name from, const vector<permission_level>& auths, uint32_t expiration ) {
fc::variants v;
for ( auto& level : auths ) {
v.push_back(fc::mutable_variant_object()
("actor", level.actor)
("permission", level.permission)
);
}
auto act_obj = fc::mutable_variant_object()
("account", name{config::system_account_name})
("name", "reqauth")
("authorization", v)
("data", fc::mutable_variant_object() ("from", from) );
transaction trx;
set_transaction_headers(trx, expiration);
action act;
abi_serializer::from_variant( act_obj, act, get_resolver() );
trx.actions.push_back( std::move(act) );
return trx;
}
BOOST_AUTO_TEST_SUITE(eosio_sudo_tests)
BOOST_FIXTURE_TEST_CASE( sudo_exec_direct, eosio_sudo_tester ) try {
auto trx = reqauth( N(bob), {permission_level{N(bob), config::active_name}} );
transaction_trace_ptr trace;
control->applied_transaction.connect([&]( const transaction_trace_ptr& t) { if (t->scheduled) { trace = t; } } );
{
signed_transaction sudo_trx( sudo_exec( N(alice), trx ), {}, {} );
/*
set_transaction_headers( sudo_trx );
sudo_trx.actions.emplace_back( get_action( N(eosio.sudo), N(exec),
{{N(alice), config::active_name}, {N(eosio.sudo), config::active_name}},
mvo()
("executer", "alice")
("trx", trx)
) );
*/
sudo_trx.sign( get_private_key( N(alice), "active" ), control->get_chain_id() );
for( const auto& actor : {"prod1", "prod2", "prod3", "prod4"} ) {
sudo_trx.sign( get_private_key( actor, "active" ), control->get_chain_id() );
}
push_transaction( sudo_trx );
}
produce_block();
BOOST_REQUIRE( bool(trace) );
BOOST_REQUIRE_EQUAL( 1, trace->action_traces.size() );
BOOST_REQUIRE_EQUAL( "eosio", name{trace->action_traces[0].act.account} );
BOOST_REQUIRE_EQUAL( "reqauth", name{trace->action_traces[0].act.name} );
BOOST_REQUIRE_EQUAL( transaction_receipt::executed, trace->receipt->status );
} FC_LOG_AND_RETHROW()
BOOST_FIXTURE_TEST_CASE( sudo_with_msig, eosio_sudo_tester ) try {
auto trx = reqauth( N(bob), {permission_level{N(bob), config::active_name}} );
auto sudo_trx = sudo_exec( N(alice), trx );
propose( N(carol), N(first),
{ {N(alice), N(active)},
{N(prod1), N(active)}, {N(prod2), N(active)}, {N(prod3), N(active)}, {N(prod4), N(active)}, {N(prod5), N(active)} },
sudo_trx );
approve( N(carol), N(first), N(alice) ); // alice must approve since she is the executer of the sudo::exec action
// More than 2/3 of block producers approve
approve( N(carol), N(first), N(prod1) );
approve( N(carol), N(first), N(prod2) );
approve( N(carol), N(first), N(prod3) );
approve( N(carol), N(first), N(prod4) );
vector<transaction_trace_ptr> traces;
control->applied_transaction.connect([&]( const transaction_trace_ptr& t) {
if (t->scheduled) {
traces.push_back( t );
}
} );
// Now the proposal should be ready to execute
push_action( N(eosio.msig), N(exec), N(alice), mvo()
("proposer", "carol")
("proposal_name", "first")
("executer", "alice")
);
produce_block();
BOOST_REQUIRE_EQUAL( 2, traces.size() );
BOOST_REQUIRE_EQUAL( 1, traces[0]->action_traces.size() );
BOOST_REQUIRE_EQUAL( "eosio.sudo", name{traces[0]->action_traces[0].act.account} );
BOOST_REQUIRE_EQUAL( "exec", name{traces[0]->action_traces[0].act.name} );
BOOST_REQUIRE_EQUAL( transaction_receipt::executed, traces[0]->receipt->status );
BOOST_REQUIRE_EQUAL( 1, traces[1]->action_traces.size() );
BOOST_REQUIRE_EQUAL( "eosio", name{traces[1]->action_traces[0].act.account} );
BOOST_REQUIRE_EQUAL( "reqauth", name{traces[1]->action_traces[0].act.name} );
BOOST_REQUIRE_EQUAL( transaction_receipt::executed, traces[1]->receipt->status );
} FC_LOG_AND_RETHROW()
BOOST_FIXTURE_TEST_CASE( sudo_with_msig_unapprove, eosio_sudo_tester ) try {
auto trx = reqauth( N(bob), {permission_level{N(bob), config::active_name}} );
auto sudo_trx = sudo_exec( N(alice), trx );
propose( N(carol), N(first),
{ {N(alice), N(active)},
{N(prod1), N(active)}, {N(prod2), N(active)}, {N(prod3), N(active)}, {N(prod4), N(active)}, {N(prod5), N(active)} },
sudo_trx );
approve( N(carol), N(first), N(alice) ); // alice must approve since she is the executer of the sudo::exec action
// 3 of the 4 needed producers approve
approve( N(carol), N(first), N(prod1) );
approve( N(carol), N(first), N(prod2) );
approve( N(carol), N(first), N(prod3) );
// first producer takes back approval
unapprove( N(carol), N(first), N(prod1) );
// fourth producer approves but the total number of approving producers is still 3 which is less than two-thirds of producers
approve( N(carol), N(first), N(prod4) );
produce_block();
// The proposal should not have sufficient approvals to pass the authorization checks of eosio.sudo::exec.
BOOST_REQUIRE_EXCEPTION( push_action( N(eosio.msig), N(exec), N(alice), mvo()
("proposer", "carol")
("proposal_name", "first")
("executer", "alice")
), eosio_assert_message_exception,
eosio_assert_message_is("transaction authorization failed")
);
} FC_LOG_AND_RETHROW()
BOOST_FIXTURE_TEST_CASE( sudo_with_msig_producers_change, eosio_sudo_tester ) try {
create_accounts( { N(newprod1) } );
auto trx = reqauth( N(bob), {permission_level{N(bob), config::active_name}} );
auto sudo_trx = sudo_exec( N(alice), trx, 36000 );
propose( N(carol), N(first),
{ {N(alice), N(active)},
{N(prod1), N(active)}, {N(prod2), N(active)}, {N(prod3), N(active)}, {N(prod4), N(active)}, {N(prod5), N(active)} },
sudo_trx );
approve( N(carol), N(first), N(alice) ); // alice must approve since she is the executer of the sudo::exec action
// 2 of the 4 needed producers approve
approve( N(carol), N(first), N(prod1) );
approve( N(carol), N(first), N(prod2) );
produce_block();
set_producers( {N(prod1), N(prod2), N(prod3), N(prod4), N(prod5), N(newprod1)} ); // With 6 producers, the 2/3+1 threshold becomes 5
while( control->pending_block_state()->active_schedule.producers.size() != 6 ) {
produce_block();
}
// Now two more block producers approve which would have been sufficient under the old schedule but not the new one.
approve( N(carol), N(first), N(prod3) );
approve( N(carol), N(first), N(prod4) );
produce_block();
// The proposal has four of the five requested approvals but they are not sufficient to satisfy the authorization checks of eosio.sudo::exec.
BOOST_REQUIRE_EXCEPTION( push_action( N(eosio.msig), N(exec), N(alice), mvo()
("proposer", "carol")
("proposal_name", "first")
("executer", "alice")
), eosio_assert_message_exception,
eosio_assert_message_is("transaction authorization failed")
);
// Unfortunately the new producer cannot approve because they were not in the original requested approvals.
BOOST_REQUIRE_EXCEPTION( approve( N(carol), N(first), N(newprod1) ),
eosio_assert_message_exception,
eosio_assert_message_is("approval is not on the list of requested approvals")
);
// But prod5 still can provide the fifth approval necessary to satisfy the 2/3+1 threshold of the new producer set
approve( N(carol), N(first), N(prod5) );
vector<transaction_trace_ptr> traces;
control->applied_transaction.connect([&]( const transaction_trace_ptr& t) {
if (t->scheduled) {
traces.push_back( t );
}
} );
// Now the proposal should be ready to execute
push_action( N(eosio.msig), N(exec), N(alice), mvo()
("proposer", "carol")
("proposal_name", "first")
("executer", "alice")
);
produce_block();
BOOST_REQUIRE_EQUAL( 2, traces.size() );
BOOST_REQUIRE_EQUAL( 1, traces[0]->action_traces.size() );
BOOST_REQUIRE_EQUAL( "eosio.sudo", name{traces[0]->action_traces[0].act.account} );
BOOST_REQUIRE_EQUAL( "exec", name{traces[0]->action_traces[0].act.name} );
BOOST_REQUIRE_EQUAL( transaction_receipt::executed, traces[0]->receipt->status );
BOOST_REQUIRE_EQUAL( 1, traces[1]->action_traces.size() );
BOOST_REQUIRE_EQUAL( "eosio", name{traces[1]->action_traces[0].act.account} );
BOOST_REQUIRE_EQUAL( "reqauth", name{traces[1]->action_traces[0].act.name} );
BOOST_REQUIRE_EQUAL( transaction_receipt::executed, traces[1]->receipt->status );
} FC_LOG_AND_RETHROW()
BOOST_AUTO_TEST_SUITE_END()
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册