зеркало из https://github.com/microsoft/CCF.git
No-op JS gov (#2350)
This commit is contained in:
Родитель
7af1179111
Коммит
e3364b7200
|
@ -27,7 +27,7 @@ parameters:
|
|||
perf:
|
||||
cmake_args: '-DBUILD_UNIT_TESTS=OFF -DDISTRIBUTE_PERF_TESTS="`../.nodes.sh`"'
|
||||
release:
|
||||
cmake_args: "-DTLS_TEST=ON -DENABLE_BFT=OFF"
|
||||
cmake_args: "-DTLS_TEST=ON -DENABLE_BFT=OFF -DENABLE_JS_GOV=OFF"
|
||||
|
||||
test:
|
||||
NoSGX:
|
||||
|
|
|
@ -60,6 +60,10 @@ option(ZAP_TEST
|
|||
"ZAP fuzz test using https://www.zaproxy.org/docs/docker/api-scan/" OFF
|
||||
)
|
||||
option(BUILD_SMALLBANK "Build SmallBank sample app and clients" ON)
|
||||
option(ENABLE_JS_GOV "Enable JS Governance" ON)
|
||||
if(ENABLE_JS_GOV)
|
||||
add_definitions(-DENABLE_JS_GOV)
|
||||
endif()
|
||||
option(BUILD_TPCC "Build TPPC sample app and clients" ON)
|
||||
|
||||
# Build common library for CCF enclaves
|
||||
|
@ -486,6 +490,15 @@ if(BUILD_TESTS)
|
|||
add_picobench(hash_bench SRCS src/ds/test/hash_bench.cpp)
|
||||
add_picobench(digest_bench SRCS src/crypto/test/digest_bench.cpp)
|
||||
|
||||
if(ENABLE_JS_GOV)
|
||||
add_e2e_test(
|
||||
NAME governance_js_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/governance_js.py
|
||||
CONSENSUS cft
|
||||
ADDITIONAL_ARGS --oe-binary ${OE_BINDIR} --initial-operator-count 1
|
||||
)
|
||||
endif()
|
||||
|
||||
# Storing signed governance operations
|
||||
add_e2e_test(
|
||||
NAME governance_history_test
|
||||
|
|
|
@ -1,6 +1,38 @@
|
|||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Action": {
|
||||
"properties": {
|
||||
"args": {
|
||||
"$ref": "#/components/schemas/json"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/components/schemas/string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"args"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Action_array": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Action"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"Ballot": {
|
||||
"properties": {
|
||||
"ballot": {
|
||||
"$ref": "#/components/schemas/string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ballot"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CodeStatus": {
|
||||
"enum": [
|
||||
"AllowedToJoin"
|
||||
|
@ -60,24 +92,6 @@
|
|||
"pattern": "^[a-f0-9]{64}$",
|
||||
"type": "string"
|
||||
},
|
||||
"EntityId_to_Script": {
|
||||
"items": {
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EntityId"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Script"
|
||||
}
|
||||
]
|
||||
},
|
||||
"maxItems": 2,
|
||||
"minItems": 2,
|
||||
"type": "array"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"GetCode__Out": {
|
||||
"properties": {
|
||||
"versions": {
|
||||
|
@ -164,28 +178,12 @@
|
|||
},
|
||||
"Proposal": {
|
||||
"properties": {
|
||||
"parameter": {
|
||||
"$ref": "#/components/schemas/json"
|
||||
},
|
||||
"proposer": {
|
||||
"$ref": "#/components/schemas/EntityId"
|
||||
},
|
||||
"script": {
|
||||
"$ref": "#/components/schemas/Script"
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/ProposalState"
|
||||
},
|
||||
"votes": {
|
||||
"$ref": "#/components/schemas/EntityId_to_Script"
|
||||
"actions": {
|
||||
"$ref": "#/components/schemas/Action_array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"script",
|
||||
"parameter",
|
||||
"proposer",
|
||||
"state",
|
||||
"votes"
|
||||
"actions"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
@ -208,6 +206,29 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProposalInfoSummary": {
|
||||
"properties": {
|
||||
"ballot_count": {
|
||||
"$ref": "#/components/schemas/uint64"
|
||||
},
|
||||
"proposal_id": {
|
||||
"$ref": "#/components/schemas/string"
|
||||
},
|
||||
"proposer_id": {
|
||||
"$ref": "#/components/schemas/EntityId"
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/ProposalState"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"proposal_id",
|
||||
"proposer_id",
|
||||
"state",
|
||||
"ballot_count"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProposalState": {
|
||||
"enum": [
|
||||
"Open",
|
||||
|
@ -539,6 +560,210 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/proposals.js": {
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Proposal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Auto-generated request body schema"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProposalInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default response description"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"member_signature": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/proposals.js/{proposal_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProposalInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default response description"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"member_signature": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "proposal_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/proposals.js/{proposal_id}/actions": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Proposal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default response description"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"member_signature": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "proposal_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/proposals.js/{proposal_id}/ballots": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "proposal_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Ballot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Auto-generated request body schema"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProposalInfoSummary"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default response description"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"member_signature": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/proposals.js/{proposal_id}/ballots/{member_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Ballot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default response description"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"member_signature": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "proposal_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "member_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/proposals.js/{proposal_id}/withdraw": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "proposal_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProposalInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Default response description"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"member_signature": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/proposals/{proposal_id}": {
|
||||
"get": {
|
||||
"responses": {
|
||||
|
|
|
@ -536,7 +536,6 @@ namespace ccfapp
|
|||
network(network),
|
||||
context(context)
|
||||
{
|
||||
js::register_class_ids();
|
||||
metrics_tracker.install_endpoint(*this);
|
||||
}
|
||||
|
||||
|
|
|
@ -523,7 +523,7 @@ namespace js
|
|||
}
|
||||
|
||||
// Historical queries
|
||||
if (receipt)
|
||||
if (receipt != nullptr)
|
||||
{
|
||||
auto state = JS_NewObject(ctx);
|
||||
|
||||
|
|
|
@ -5,26 +5,69 @@
|
|||
#include "ds/json.h"
|
||||
#include "entities.h"
|
||||
#include "kv/map.h"
|
||||
#include "proposals.h"
|
||||
|
||||
namespace ccf
|
||||
{
|
||||
namespace jsgov
|
||||
{
|
||||
using ProposalId = std::string;
|
||||
using Proposal = std::string;
|
||||
|
||||
struct ProposalInfo
|
||||
{
|
||||
ccf::MemberId proposer_id;
|
||||
ccf::ProposalState state;
|
||||
std::unordered_map<ccf::MemberId, std::string> ballots = {};
|
||||
};
|
||||
DECLARE_JSON_TYPE(ProposalInfo)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(ProposalInfo, proposer_id, ballots);
|
||||
DECLARE_JSON_TYPE(ProposalInfo);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(ProposalInfo, proposer_id, state, ballots);
|
||||
|
||||
using ProposalMap = kv::RawCopySerialisedMap<ProposalId, Proposal>;
|
||||
using ProposalInfoMap = kv::MapSerialisedWith<
|
||||
ProposalId,
|
||||
ProposalInfo,
|
||||
kv::serialisers::BlitSerialiser,
|
||||
kv::serialisers::JsonSerialiser>;
|
||||
struct ProposalInfoSummary
|
||||
{
|
||||
ProposalId proposal_id;
|
||||
ccf::MemberId proposer_id;
|
||||
ccf::ProposalState state;
|
||||
size_t ballot_count;
|
||||
};
|
||||
DECLARE_JSON_TYPE(ProposalInfoSummary);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(
|
||||
ProposalInfoSummary, proposal_id, proposer_id, state, ballot_count);
|
||||
|
||||
struct ProposalInfoDetails
|
||||
{
|
||||
ProposalId proposal_id;
|
||||
ccf::MemberId proposer_id;
|
||||
ccf::ProposalState state;
|
||||
std::unordered_map<ccf::MemberId, std::string> ballots = {};
|
||||
};
|
||||
DECLARE_JSON_TYPE(ProposalInfoDetails);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(
|
||||
ProposalInfoDetails, proposal_id, proposer_id, state, ballots);
|
||||
|
||||
using ProposalMap =
|
||||
kv::RawCopySerialisedMap<ProposalId, std::vector<uint8_t>>;
|
||||
using ProposalInfoMap = ServiceMap<ProposalId, ProposalInfo>;
|
||||
|
||||
struct Action
|
||||
{
|
||||
std::string name;
|
||||
nlohmann::json args;
|
||||
};
|
||||
DECLARE_JSON_TYPE(Action);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(Action, name, args);
|
||||
|
||||
struct Proposal
|
||||
{
|
||||
std::vector<Action> actions;
|
||||
};
|
||||
DECLARE_JSON_TYPE(Proposal);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(Proposal, actions);
|
||||
|
||||
struct Ballot
|
||||
{
|
||||
std::string ballot;
|
||||
};
|
||||
DECLARE_JSON_TYPE(Ballot);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(Ballot, ballot);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
#include "genesis_gen.h"
|
||||
#include "history.h"
|
||||
#include "hooks.h"
|
||||
#include "js/wrap.h"
|
||||
#include "network_state.h"
|
||||
#include "node/jwt_key_auto_refresh.h"
|
||||
#include "node/progress_tracker.h"
|
||||
|
@ -343,6 +344,7 @@ namespace ccf
|
|||
|
||||
config = std::move(config_);
|
||||
|
||||
js::register_class_ids();
|
||||
open_frontend(ActorsType::nodes);
|
||||
|
||||
#ifdef GET_QUOTE
|
||||
|
|
|
@ -482,7 +482,12 @@ namespace ccf
|
|||
|
||||
void set_root_on_proposals(const enclave::RpcContext& ctx, kv::Tx& tx)
|
||||
{
|
||||
if (ctx.get_request_path() == "/gov/proposals")
|
||||
if (
|
||||
ctx.get_request_path() == "/gov/proposals"
|
||||
#ifdef ENABLE_JS_GOV
|
||||
|| ctx.get_request_path() == "/gov/proposals.js"
|
||||
#endif
|
||||
)
|
||||
{
|
||||
update_history();
|
||||
if (history)
|
||||
|
|
|
@ -1152,6 +1152,152 @@ namespace ccf
|
|||
return get_proposal_info(proposal_id, proposal);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_JS_GOV
|
||||
|
||||
# pragma clang diagnostic push
|
||||
# pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
||||
ccf::jsgov::ProposalInfoSummary resolve_proposal(
|
||||
kv::Tx& tx,
|
||||
const ProposalId& proposal_id,
|
||||
const std::vector<uint8_t>& proposal,
|
||||
const std::string& constitution)
|
||||
{
|
||||
auto pi =
|
||||
tx.rw<ccf::jsgov::ProposalInfoMap>("public:ccf.gov.proposals_info.js");
|
||||
auto pi_ = pi->get(proposal_id);
|
||||
|
||||
std::vector<std::pair<MemberId, bool>> votes;
|
||||
for (const auto& [mid, mb] : pi_->ballots)
|
||||
{
|
||||
std::string mbs = fmt::format(
|
||||
"{}\n export default (proposal, proposer_id) => vote(proposal, "
|
||||
"proposer_id);",
|
||||
mb);
|
||||
|
||||
js::Runtime rt;
|
||||
js::Context context(rt);
|
||||
rt.add_ccf_classdefs();
|
||||
js::populate_global_ccf(&tx, std::nullopt, nullptr, context);
|
||||
auto ballot_func = context.function(
|
||||
mbs, fmt::format("ballot from {} for {}", mid, proposal_id));
|
||||
|
||||
JSValue argv[2];
|
||||
auto prop = JS_NewStringLen(
|
||||
context, (const char*)proposal.data(), proposal.size());
|
||||
argv[0] = prop;
|
||||
auto pid = JS_NewStringLen(
|
||||
context, pi_->proposer_id.data(), pi_->proposer_id.size());
|
||||
argv[1] = pid;
|
||||
|
||||
auto val =
|
||||
context(JS_Call(context, ballot_func, JS_UNDEFINED, 2, argv));
|
||||
if (!JS_IsException(val))
|
||||
{
|
||||
votes.emplace_back(mid, JS_ToBool(context, val));
|
||||
}
|
||||
JS_FreeValue(context, ballot_func);
|
||||
JS_FreeValue(context, prop);
|
||||
JS_FreeValue(context, pid);
|
||||
}
|
||||
|
||||
{
|
||||
std::string mbs = fmt::format(
|
||||
"{}\n export default (proposal, proposer_id, votes) => "
|
||||
"resolve(proposal, proposer_id, votes);",
|
||||
constitution);
|
||||
|
||||
js::Runtime rt;
|
||||
js::Context context(rt);
|
||||
js::populate_global_console(context);
|
||||
rt.add_ccf_classdefs();
|
||||
js::populate_global_ccf(&tx, std::nullopt, nullptr, context);
|
||||
auto resolve_func =
|
||||
context.function(mbs, fmt::format("resolve {}", proposal_id));
|
||||
JSValue argv[3];
|
||||
auto prop = JS_NewStringLen(
|
||||
context, (const char*)proposal.data(), proposal.size());
|
||||
argv[0] = prop;
|
||||
|
||||
auto prop_id = JS_NewStringLen(
|
||||
context, pi_->proposer_id.data(), pi_->proposer_id.size());
|
||||
argv[1] = prop_id;
|
||||
|
||||
auto vs = JS_NewArray(context);
|
||||
size_t index = 0;
|
||||
for (auto& [mid, vote] : votes)
|
||||
{
|
||||
auto v = JS_NewObject(context);
|
||||
auto member_id = JS_NewStringLen(context, mid.data(), mid.size());
|
||||
JS_DefinePropertyValueStr(
|
||||
context, v, "member_id", member_id, JS_PROP_C_W_E);
|
||||
auto vote_status = JS_NewBool(context, vote);
|
||||
JS_DefinePropertyValueStr(
|
||||
context, v, "vote", vote_status, JS_PROP_C_W_E);
|
||||
JS_DefinePropertyValueUint32(context, vs, index++, v, JS_PROP_C_W_E);
|
||||
}
|
||||
argv[2] = vs;
|
||||
|
||||
auto val =
|
||||
context(JS_Call(context, resolve_func, JS_UNDEFINED, 3, argv));
|
||||
|
||||
JS_FreeValue(context, resolve_func);
|
||||
JS_FreeValue(context, prop);
|
||||
JS_FreeValue(context, prop_id);
|
||||
JS_FreeValue(context, vs);
|
||||
|
||||
if (JS_IsString(val))
|
||||
{
|
||||
auto s = JS_ToCString(context, val);
|
||||
std::string status(s);
|
||||
JS_FreeCString(context, s);
|
||||
if (status == "Open")
|
||||
{
|
||||
pi_.value().state = ProposalState::OPEN;
|
||||
}
|
||||
else if (status == "Accepted")
|
||||
{
|
||||
pi_.value().state = ProposalState::ACCEPTED;
|
||||
}
|
||||
else if (status == "Withdrawn")
|
||||
{
|
||||
pi_.value().state = ProposalState::FAILED;
|
||||
}
|
||||
else if (status == "Rejected")
|
||||
{
|
||||
pi_.value().state = ProposalState::REJECTED;
|
||||
}
|
||||
else if (status == "Failed")
|
||||
{
|
||||
pi_.value().state = ProposalState::FAILED;
|
||||
}
|
||||
else
|
||||
{
|
||||
pi_.value().state = ProposalState::FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply actions
|
||||
if (pi_.value().state != ProposalState::OPEN)
|
||||
{
|
||||
// Record votes and errors
|
||||
if (pi_.value().state == ProposalState::ACCEPTED)
|
||||
{
|
||||
// Apply actions here
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return jsgov::ProposalInfoSummary{proposal_id,
|
||||
pi_->proposer_id,
|
||||
pi_.value().state,
|
||||
pi_.value().ballots.size()};
|
||||
}
|
||||
}
|
||||
|
||||
# pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
bool check_member_active(kv::ReadOnlyTx& tx, const MemberId& id)
|
||||
{
|
||||
return check_member_status(tx, id, {MemberStatus::ACTIVE});
|
||||
|
@ -2110,6 +2256,542 @@ namespace ccf
|
|||
{std::make_shared<NodeCertAuthnPolicy>()})
|
||||
.set_openapi_hidden(true)
|
||||
.install();
|
||||
|
||||
// JavaScript governance
|
||||
#ifdef ENABLE_JS_GOV
|
||||
|
||||
# pragma clang diagnostic push
|
||||
# pragma clang diagnostic ignored "-Wc99-extensions"
|
||||
|
||||
auto post_proposals_js = [this](ccf::endpoints::EndpointContext& ctx) {
|
||||
const auto& caller_identity =
|
||||
ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
|
||||
if (!check_member_active(ctx.tx, caller_identity.member_id))
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
ccf::errors::AuthorizationFailed,
|
||||
"Member is not active.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consensus)
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"No consensus available.");
|
||||
return;
|
||||
}
|
||||
|
||||
ProposalId proposal_id;
|
||||
if (consensus->type() == ConsensusType::CFT)
|
||||
{
|
||||
auto root_at_read = ctx.tx.get_root_at_read_version();
|
||||
if (!root_at_read.has_value())
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Proposal failed to bind to state.");
|
||||
return;
|
||||
}
|
||||
|
||||
// caller_identity.request_digest is set when getting the
|
||||
// MemberSignatureAuthnIdentity identity. The proposal id is a
|
||||
// digest of the root of the state tree at the read version and the
|
||||
// request digest.
|
||||
std::vector<uint8_t> acc(
|
||||
root_at_read.value().h.begin(), root_at_read.value().h.end());
|
||||
acc.insert(
|
||||
acc.end(),
|
||||
caller_identity.request_digest.begin(),
|
||||
caller_identity.request_digest.end());
|
||||
const crypto::Sha256Hash proposal_digest(acc);
|
||||
proposal_id = proposal_digest.hex_str();
|
||||
}
|
||||
else
|
||||
{
|
||||
proposal_id = fmt::format(
|
||||
"{:02x}", fmt::join(caller_identity.request_digest, ""));
|
||||
}
|
||||
|
||||
js::Runtime rt;
|
||||
js::Context context(rt);
|
||||
auto constitution = ctx.tx.ro(network.constitution)->get(0);
|
||||
if (!constitution.has_value())
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"No constitution is set - proposals cannot be evaluated");
|
||||
return;
|
||||
}
|
||||
|
||||
auto validate_script = fmt::format(
|
||||
"{}\n export default (input) => validate(input);",
|
||||
constitution.value());
|
||||
|
||||
auto validate_func = context.function(
|
||||
validate_script, "public:ccf.gov.constitution[0].validate");
|
||||
|
||||
auto body =
|
||||
reinterpret_cast<const char*>(ctx.rpc_ctx->get_request_body().data());
|
||||
auto body_len = ctx.rpc_ctx->get_request_body().size();
|
||||
|
||||
auto proposal = JS_NewStringLen(context, body, body_len);
|
||||
JSValueConst* argv = (JSValueConst*)&proposal;
|
||||
|
||||
auto val =
|
||||
context(JS_Call(context, validate_func, JS_UNDEFINED, 1, argv));
|
||||
|
||||
JS_FreeValue(context, proposal);
|
||||
JS_FreeValue(context, validate_func);
|
||||
|
||||
if (JS_IsException(val))
|
||||
{
|
||||
js::js_dump_error(context);
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Failed to execute validation");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!JS_IsObject(val))
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Validation failed to return an object");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string description;
|
||||
auto desc = context(JS_GetPropertyStr(context, val, "description"));
|
||||
if (JS_IsString(desc))
|
||||
{
|
||||
auto cstr = JS_ToCString(context, desc);
|
||||
description = std::string(cstr);
|
||||
JS_FreeCString(context, cstr);
|
||||
}
|
||||
|
||||
auto valid = context(JS_GetPropertyStr(context, val, "valid"));
|
||||
if (!JS_ToBool(context, valid))
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
ccf::errors::ProposalFailedToValidate,
|
||||
fmt::format("Proposal failed to validate: {}", description));
|
||||
return;
|
||||
}
|
||||
|
||||
auto pm =
|
||||
ctx.tx.rw<ccf::jsgov::ProposalMap>("public:ccf.gov.proposals.js");
|
||||
// Introduce a read dependency, so that if identical proposal
|
||||
// creations are in-flight and reading at the same version, all except
|
||||
// the first conflict and are re-executed. If we ever produce a
|
||||
// proposal ID which already exists, we must have a hash collision.
|
||||
if (pm->has(proposal_id))
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"Proposal ID collision.");
|
||||
return;
|
||||
}
|
||||
pm->put(proposal_id, ctx.rpc_ctx->get_request_body());
|
||||
|
||||
auto pi = ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(
|
||||
"public:ccf.gov.proposals_info.js");
|
||||
pi->put(
|
||||
proposal_id,
|
||||
{caller_identity.member_id, ccf::ProposalState::OPEN, {}});
|
||||
|
||||
record_voting_history(
|
||||
ctx.tx, caller_identity.member_id, caller_identity.signed_request);
|
||||
|
||||
auto rv = resolve_proposal(
|
||||
ctx.tx,
|
||||
proposal_id,
|
||||
ctx.rpc_ctx->get_request_body(),
|
||||
constitution.value());
|
||||
pi->put(proposal_id, {caller_identity.member_id, rv.state, {}});
|
||||
|
||||
ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
|
||||
ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
|
||||
ctx.rpc_ctx->set_response_body(nlohmann::json(rv).dump());
|
||||
};
|
||||
|
||||
make_endpoint(
|
||||
"proposals.js", HTTP_POST, post_proposals_js, member_sig_only)
|
||||
.set_auto_schema<jsgov::Proposal, jsgov::ProposalInfo>()
|
||||
.install();
|
||||
|
||||
auto get_proposal_js =
|
||||
[this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
|
||||
const auto& caller_identity =
|
||||
ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
|
||||
if (!check_member_active(ctx.tx, caller_identity.member_id))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
ccf::errors::AuthorizationFailed,
|
||||
"Member is not active.");
|
||||
}
|
||||
|
||||
// Take expand=ballots, return eg. "ballots": 3 if not set
|
||||
// or "ballots": list of ballots in full if passed
|
||||
|
||||
ProposalId proposal_id;
|
||||
std::string error;
|
||||
if (!get_proposal_id_from_path(
|
||||
ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
|
||||
}
|
||||
|
||||
auto pm =
|
||||
ctx.tx.ro<ccf::jsgov::ProposalMap>("public:ccf.gov.proposals.js");
|
||||
auto p = pm->get(proposal_id);
|
||||
|
||||
if (!p)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_NOT_FOUND,
|
||||
ccf::errors::ProposalNotFound,
|
||||
fmt::format("Proposal {} does not exist.", proposal_id));
|
||||
}
|
||||
|
||||
auto pi = ctx.tx.ro<ccf::jsgov::ProposalInfoMap>(
|
||||
"public:ccf.gov.proposals_info.js");
|
||||
auto pi_ = pi->get(proposal_id);
|
||||
|
||||
if (!pi_)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
fmt::format(
|
||||
"No proposal info associated with {} exists.", proposal_id));
|
||||
}
|
||||
|
||||
return make_success(pi_.value());
|
||||
};
|
||||
|
||||
make_read_only_endpoint(
|
||||
"proposals.js/{proposal_id}",
|
||||
HTTP_GET,
|
||||
json_read_only_adapter(get_proposal_js),
|
||||
member_cert_or_sig)
|
||||
.set_auto_schema<void, jsgov::ProposalInfo>()
|
||||
.install();
|
||||
|
||||
auto withdraw_js = [this](
|
||||
endpoints::EndpointContext& ctx, nlohmann::json&&) {
|
||||
const auto& caller_identity =
|
||||
ctx.template get_caller<ccf::MemberSignatureAuthnIdentity>();
|
||||
if (!check_member_active(ctx.tx, caller_identity.member_id))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
ccf::errors::AuthorizationFailed,
|
||||
"Member is not active.");
|
||||
}
|
||||
|
||||
ProposalId proposal_id;
|
||||
std::string error;
|
||||
if (!get_proposal_id_from_path(
|
||||
ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
|
||||
}
|
||||
|
||||
auto pi = ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(
|
||||
"public:ccf.gov.proposals_info.js");
|
||||
auto pi_ = pi->get(proposal_id);
|
||||
|
||||
if (!pi_)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
ccf::errors::ProposalNotFound,
|
||||
fmt::format("Proposal {} does not exist.", proposal_id));
|
||||
}
|
||||
|
||||
if (caller_identity.member_id != pi_->proposer_id)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
ccf::errors::AuthorizationFailed,
|
||||
fmt::format(
|
||||
"Proposal {} can only be withdrawn by proposer {}, not caller "
|
||||
"{}.",
|
||||
proposal_id,
|
||||
pi_->proposer_id,
|
||||
caller_identity.member_id));
|
||||
}
|
||||
|
||||
if (pi_->state != ProposalState::OPEN)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
ccf::errors::ProposalNotOpen,
|
||||
fmt::format(
|
||||
"Proposal {} is currently in state {} - only {} proposals can be "
|
||||
"withdrawn.",
|
||||
proposal_id,
|
||||
pi_->state,
|
||||
ProposalState::OPEN));
|
||||
}
|
||||
|
||||
pi_->state = ProposalState::WITHDRAWN;
|
||||
pi->put(proposal_id, pi_.value());
|
||||
|
||||
record_voting_history(
|
||||
ctx.tx, caller_identity.member_id, caller_identity.signed_request);
|
||||
|
||||
return make_success(pi_.value());
|
||||
};
|
||||
|
||||
make_endpoint(
|
||||
"proposals.js/{proposal_id}/withdraw",
|
||||
HTTP_POST,
|
||||
json_adapter(withdraw_js),
|
||||
member_cert_or_sig)
|
||||
.set_auto_schema<void, jsgov::ProposalInfo>()
|
||||
.install();
|
||||
|
||||
auto get_proposal_actions_js =
|
||||
[this](ccf::endpoints::ReadOnlyEndpointContext& ctx) {
|
||||
const auto& caller_identity =
|
||||
ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
|
||||
if (!check_member_active(ctx.tx, caller_identity.member_id))
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
ccf::errors::AuthorizationFailed,
|
||||
"Member is not active.");
|
||||
return;
|
||||
}
|
||||
|
||||
ProposalId proposal_id;
|
||||
std::string error;
|
||||
if (!get_proposal_id_from_path(
|
||||
ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
ccf::errors::InvalidResourceName,
|
||||
std::move(error));
|
||||
return;
|
||||
}
|
||||
|
||||
auto pm =
|
||||
ctx.tx.ro<ccf::jsgov::ProposalMap>("public:ccf.gov.proposals.js");
|
||||
auto p = pm->get(proposal_id);
|
||||
|
||||
if (!p)
|
||||
{
|
||||
ctx.rpc_ctx->set_error(
|
||||
HTTP_STATUS_NOT_FOUND,
|
||||
ccf::errors::ProposalNotFound,
|
||||
fmt::format("Proposal {} does not exist.", proposal_id));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
|
||||
ctx.rpc_ctx->set_response_header(
|
||||
http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
|
||||
ctx.rpc_ctx->set_response_body(std::move(p.value()));
|
||||
};
|
||||
|
||||
make_read_only_endpoint(
|
||||
"proposals.js/{proposal_id}/actions",
|
||||
HTTP_GET,
|
||||
get_proposal_actions_js,
|
||||
member_cert_or_sig)
|
||||
.set_auto_schema<void, jsgov::Proposal>()
|
||||
.install();
|
||||
|
||||
auto vote_js = [this](
|
||||
endpoints::EndpointContext& ctx,
|
||||
nlohmann::json&& params) {
|
||||
const auto& caller_identity =
|
||||
ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
|
||||
if (!check_member_active(ctx.tx, caller_identity.member_id))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
ccf::errors::AuthorizationFailed,
|
||||
"Member is not active.");
|
||||
}
|
||||
|
||||
ProposalId proposal_id;
|
||||
std::string error;
|
||||
if (!get_proposal_id_from_path(
|
||||
ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
|
||||
}
|
||||
|
||||
auto constitution = ctx.tx.ro(network.constitution)->get(0);
|
||||
if (!constitution.has_value())
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
||||
ccf::errors::InternalError,
|
||||
"No constitution is set - proposals cannot be evaluated");
|
||||
}
|
||||
|
||||
auto pi = ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(
|
||||
"public:ccf.gov.proposals_info.js");
|
||||
auto pi_ = pi->get(proposal_id);
|
||||
if (!pi_)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_NOT_FOUND,
|
||||
ccf::errors::ProposalNotFound,
|
||||
fmt::format("Could not find proposal {}.", proposal_id));
|
||||
}
|
||||
|
||||
if (pi_.value().state != ProposalState::OPEN)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
ccf::errors::ProposalNotOpen,
|
||||
fmt::format(
|
||||
"Proposal {} is currently in state {} - only {} proposals can "
|
||||
"receive votes.",
|
||||
proposal_id,
|
||||
pi_.value().state,
|
||||
ProposalState::OPEN));
|
||||
}
|
||||
|
||||
auto pm =
|
||||
ctx.tx.ro<ccf::jsgov::ProposalMap>("public:ccf.gov.proposals.js");
|
||||
auto p = pm->get(proposal_id);
|
||||
|
||||
if (!p)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_NOT_FOUND,
|
||||
ccf::errors::ProposalNotFound,
|
||||
fmt::format("Proposal {} does not exist.", proposal_id));
|
||||
}
|
||||
|
||||
if (pi_->ballots.find(caller_identity.member_id) != pi_->ballots.end())
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST,
|
||||
ccf::errors::VoteAlreadyExists,
|
||||
"Vote already submitted.");
|
||||
}
|
||||
// Validate vote
|
||||
|
||||
std::string ballot_script = fmt::format(
|
||||
"{}\n export default (proposal, proposer_id, tx) => vote(proposal, "
|
||||
"proposer_id, tx);",
|
||||
params["ballot"]);
|
||||
|
||||
{
|
||||
js::Runtime rt;
|
||||
js::Context context(rt);
|
||||
auto ballot_func =
|
||||
context.function(ballot_script, "body[\"ballot\"]");
|
||||
JS_FreeValue(context, ballot_func);
|
||||
}
|
||||
|
||||
pi_->ballots[caller_identity.member_id] = params["ballot"];
|
||||
pi->put(proposal_id, pi_.value());
|
||||
|
||||
// Do we still need to do this?
|
||||
record_voting_history(
|
||||
ctx.tx, caller_identity.member_id, caller_identity.signed_request);
|
||||
|
||||
auto rv = resolve_proposal(
|
||||
ctx.tx, proposal_id, p.value(), constitution.value());
|
||||
pi_.value().state = rv.state;
|
||||
pi->put(proposal_id, pi_.value());
|
||||
return make_success(rv);
|
||||
};
|
||||
make_endpoint(
|
||||
"proposals.js/{proposal_id}/ballots",
|
||||
HTTP_POST,
|
||||
json_adapter(vote_js),
|
||||
member_sig_only)
|
||||
.set_auto_schema<jsgov::Ballot, jsgov::ProposalInfoSummary>()
|
||||
.install();
|
||||
|
||||
auto get_vote_js =
|
||||
[this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
|
||||
const auto& caller_identity =
|
||||
ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
|
||||
if (!check_member_active(ctx.tx, caller_identity.member_id))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_FORBIDDEN,
|
||||
ccf::errors::AuthorizationFailed,
|
||||
"Member is not active.");
|
||||
}
|
||||
|
||||
std::string error;
|
||||
ProposalId proposal_id;
|
||||
if (!get_proposal_id_from_path(
|
||||
ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
|
||||
}
|
||||
|
||||
MemberId vote_member_id;
|
||||
if (!get_member_id_from_path(
|
||||
ctx.rpc_ctx->get_request_path_params(), vote_member_id, error))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
|
||||
}
|
||||
|
||||
auto pi = ctx.tx.ro<ccf::jsgov::ProposalInfoMap>(
|
||||
"public:ccf.gov.proposals_info.js");
|
||||
auto pi_ = pi->get(proposal_id);
|
||||
if (!pi_)
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_NOT_FOUND,
|
||||
ccf::errors::ProposalNotFound,
|
||||
fmt::format("Proposal {} does not exist.", proposal_id));
|
||||
}
|
||||
|
||||
const auto vote_it = pi_->ballots.find(vote_member_id);
|
||||
if (vote_it == pi_->ballots.end())
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_NOT_FOUND,
|
||||
ccf::errors::VoteNotFound,
|
||||
fmt::format(
|
||||
"Member {} has not voted for proposal {}.",
|
||||
vote_member_id,
|
||||
proposal_id));
|
||||
}
|
||||
|
||||
return make_success(jsgov::Ballot{vote_it->second});
|
||||
};
|
||||
make_read_only_endpoint(
|
||||
"proposals.js/{proposal_id}/ballots/{member_id}",
|
||||
HTTP_GET,
|
||||
json_read_only_adapter(get_vote_js),
|
||||
member_cert_or_sig)
|
||||
.set_auto_schema<void, jsgov::Ballot>()
|
||||
.install();
|
||||
|
||||
# pragma clang diagnostic pop
|
||||
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
class Action {
|
||||
constructor(validate, apply) {
|
||||
this.validate = validate;
|
||||
this.apply = apply;
|
||||
}
|
||||
}
|
||||
|
||||
const actions = new Map([
|
||||
[
|
||||
"set_recovery_threshold",
|
||||
new Action(
|
||||
function (args) {
|
||||
return (
|
||||
Number.isInteger(args.threshold) &&
|
||||
args.threshold > 0 &&
|
||||
args.threshold < 255
|
||||
);
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_accept_noop",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_reject_noop",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_accept_with_one_vote",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_reject_with_one_vote",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_accept_if_voted_by_operator",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_accept_if_proposed_by_operator",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_accept_with_two_votes",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
"always_reject_with_two_votes",
|
||||
new Action(
|
||||
function (args) {
|
||||
return true;
|
||||
},
|
||||
function (args, tx) {
|
||||
return true;
|
||||
}
|
||||
),
|
||||
],
|
||||
]);
|
||||
|
||||
function validate(input) {
|
||||
let proposal = JSON.parse(input);
|
||||
let errors = [];
|
||||
let position = 0;
|
||||
for (const action of proposal["actions"]) {
|
||||
const definition = actions.get(action.name);
|
||||
if (definition) {
|
||||
if (!definition.validate(action.args)) {
|
||||
errors.push(`${action.name} at position ${position} failed validation`);
|
||||
}
|
||||
} else {
|
||||
errors.push(`${action.name}: no such action`);
|
||||
}
|
||||
position++;
|
||||
}
|
||||
return { valid: errors.length === 0, description: errors.join(", ") };
|
||||
}
|
||||
|
||||
function resolve(proposal, proposer_id, votes) {
|
||||
const actions = JSON.parse(proposal)["actions"];
|
||||
if (actions.length === 1) {
|
||||
if (actions[0].name === "always_accept_noop") {
|
||||
return "Accepted";
|
||||
}
|
||||
if (actions[0].name === "always_reject_noop") {
|
||||
return "Rejected";
|
||||
}
|
||||
if (
|
||||
actions[0].name === "always_accept_with_one_vote" &&
|
||||
votes.length === 1 &&
|
||||
votes[0].vote === true
|
||||
) {
|
||||
return "Accepted";
|
||||
}
|
||||
if (
|
||||
actions[0].name === "always_reject_with_one_vote" &&
|
||||
votes.length === 1 &&
|
||||
votes[0].vote === false
|
||||
) {
|
||||
return "Rejected";
|
||||
}
|
||||
if (actions[0].name === "always_accept_if_voted_by_operator") {
|
||||
for (const vote of votes) {
|
||||
const mi = ccf.kv["public:ccf.gov.members.info"].get(
|
||||
ccf.strToBuf(vote.member_id)
|
||||
);
|
||||
if (mi && ccf.bufToJsonCompatible(mi).member_data.is_operator) {
|
||||
return "Accepted";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (actions[0].name === "always_accept_if_proposed_by_operator") {
|
||||
const mi = ccf.kv["public:ccf.gov.members.info"].get(
|
||||
ccf.strToBuf(proposer_id)
|
||||
);
|
||||
if (mi && ccf.bufToJsonCompatible(mi).member_data.is_operator) {
|
||||
return "Accepted";
|
||||
}
|
||||
}
|
||||
if (
|
||||
actions[0].name === "always_accept_with_two_votes" &&
|
||||
votes.length === 2 &&
|
||||
votes[0].vote === true &&
|
||||
votes[1].vote === true
|
||||
) {
|
||||
return "Accepted";
|
||||
}
|
||||
if (
|
||||
actions[0].name === "always_reject_with_two_votes" &&
|
||||
votes.length === 2 &&
|
||||
votes[0].vote === false &&
|
||||
votes[1].vote === false
|
||||
) {
|
||||
return "Rejected";
|
||||
}
|
||||
}
|
||||
|
||||
return "Open";
|
||||
}
|
||||
|
||||
function apply(proposal) {
|
||||
const proposed_actions = JSON.parse(proposal)["actions"];
|
||||
for (proposed_action of proposed_actions) {
|
||||
const definition = actions.get(proposed_action.name);
|
||||
definition.apply(proposed_action.args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the Apache 2.0 License.
|
||||
import infra.network
|
||||
import infra.path
|
||||
import infra.proc
|
||||
import infra.net
|
||||
import infra.e2e_args
|
||||
import suite.test_requirements as reqs
|
||||
|
||||
|
||||
def action(name, **args):
|
||||
return {"name": name, "args": args}
|
||||
|
||||
|
||||
def proposal(*actions):
|
||||
return {"actions": list(actions)}
|
||||
|
||||
|
||||
def merge(*proposals):
|
||||
return {"actions": sum((prop["actions"] for prop in proposals), [])}
|
||||
|
||||
|
||||
valid_set_recovery_threshold = proposal(action("set_recovery_threshold", threshold=5))
|
||||
valid_set_recovery_threshold_twice = merge(
|
||||
valid_set_recovery_threshold, valid_set_recovery_threshold
|
||||
)
|
||||
no_args_set_recovery_threshold = proposal(action("set_recovery_threshold"))
|
||||
bad_arg_set_recovery_threshold = proposal(
|
||||
action("set_recovery_threshold", threshold=5000)
|
||||
)
|
||||
always_accept_noop = proposal(action("always_accept_noop"))
|
||||
always_reject_noop = proposal(action("always_reject_noop"))
|
||||
always_accept_with_one_vote = proposal(action("always_accept_with_one_vote"))
|
||||
always_reject_with_one_vote = proposal(action("always_reject_with_one_vote"))
|
||||
always_accept_if_voted_by_operator = proposal(
|
||||
action("always_accept_if_voted_by_operator")
|
||||
)
|
||||
always_accept_if_proposed_by_operator = proposal(
|
||||
action("always_accept_if_proposed_by_operator")
|
||||
)
|
||||
always_accept_with_two_votes = proposal(action("always_accept_with_two_votes"))
|
||||
always_reject_with_two_votes = proposal(action("always_reject_with_two_votes"))
|
||||
|
||||
|
||||
@reqs.description("Test proposal validation")
|
||||
def test_proposal_validation(network, args):
|
||||
node = network.find_random_node()
|
||||
|
||||
with node.client(None, "member0") as c:
|
||||
r = c.post("/gov/proposals.js", valid_set_recovery_threshold)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
|
||||
r = c.post("/gov/proposals.js", valid_set_recovery_threshold_twice)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
|
||||
r = c.post("/gov/proposals.js", no_args_set_recovery_threshold)
|
||||
assert (
|
||||
r.status_code == 400
|
||||
and r.body.json()["error"]["code"] == "ProposalFailedToValidate"
|
||||
), r.body.text()
|
||||
|
||||
r = c.post(
|
||||
"/gov/proposals.js",
|
||||
merge(no_args_set_recovery_threshold, bad_arg_set_recovery_threshold),
|
||||
)
|
||||
assert (
|
||||
r.status_code == 400
|
||||
and r.body.json()["error"]["code"] == "ProposalFailedToValidate"
|
||||
), r.body.text()
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Test proposal storage")
|
||||
def test_proposal_storage(network, args):
|
||||
node = network.find_random_node()
|
||||
|
||||
with node.client(None, "member0") as c:
|
||||
r = c.get("/gov/proposals.js/42")
|
||||
assert r.status_code == 404, r.body.text()
|
||||
|
||||
r = c.get("/gov/proposals.js/42/actions")
|
||||
assert r.status_code == 404, r.body.text()
|
||||
|
||||
for prop in (valid_set_recovery_threshold, valid_set_recovery_threshold_twice):
|
||||
r = c.post("/gov/proposals.js", prop)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
r = c.get(f"/gov/proposals.js/{proposal_id}")
|
||||
assert r.status_code == 200, r.body.text()
|
||||
expected = {
|
||||
"proposer_id": network.consortium.get_member_by_local_id(
|
||||
"member0"
|
||||
).service_id,
|
||||
"state": "Open",
|
||||
"ballots": [],
|
||||
}
|
||||
assert r.body.json() == expected, r.body.json()
|
||||
|
||||
r = c.get(f"/gov/proposals.js/{proposal_id}/actions")
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json() == prop, r.body.json()
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Test proposal withdrawal")
|
||||
def test_proposal_withdrawal(network, args):
|
||||
node = network.find_random_node()
|
||||
|
||||
with node.client(None, "member0") as c:
|
||||
for prop in (valid_set_recovery_threshold, valid_set_recovery_threshold_twice):
|
||||
r = c.post("/gov/proposals.js/42/withdraw")
|
||||
assert r.status_code == 400, r.body.text()
|
||||
|
||||
r = c.post("/gov/proposals.js", prop)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
with node.client(None, "member1") as oc:
|
||||
r = oc.post(f"/gov/proposals.js/{proposal_id}/withdraw")
|
||||
assert r.status_code == 403, r.body.text()
|
||||
|
||||
r = c.get(f"/gov/proposals.js/{proposal_id}")
|
||||
assert r.status_code == 200, r.body.text()
|
||||
expected = {
|
||||
"proposer_id": network.consortium.get_member_by_local_id(
|
||||
"member0"
|
||||
).service_id,
|
||||
"state": "Open",
|
||||
"ballots": [],
|
||||
}
|
||||
assert r.body.json() == expected, r.body.json()
|
||||
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/withdraw")
|
||||
assert r.status_code == 200, r.body.text()
|
||||
expected = {
|
||||
"proposer_id": network.consortium.get_member_by_local_id(
|
||||
"member0"
|
||||
).service_id,
|
||||
"state": "Withdrawn",
|
||||
"ballots": [],
|
||||
}
|
||||
assert r.body.json() == expected, r.body.json()
|
||||
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/withdraw")
|
||||
assert r.status_code == 400, r.body.text()
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Test ballot storage and validation")
|
||||
def test_ballot_storage(network, args):
|
||||
node = network.find_random_node()
|
||||
|
||||
with node.client(None, "member0") as c:
|
||||
r = c.post("/gov/proposals.js", valid_set_recovery_threshold)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", {})
|
||||
assert r.status_code == 400, r.body.text()
|
||||
|
||||
ballot = {"ballot": "function vote (proposal, proposer_id) { return true }"}
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
|
||||
member_id = network.consortium.get_member_by_local_id("member0").service_id
|
||||
r = c.get(f"/gov/proposals.js/{proposal_id}/ballots/{member_id}")
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json() == ballot, r.body.json()
|
||||
|
||||
with node.client(None, "member1") as c:
|
||||
ballot = {"ballot": "function vote (proposal, proposer_id) { return false }"}
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
member_id = network.consortium.get_member_by_local_id("member1").service_id
|
||||
r = c.get(f"/gov/proposals.js/{proposal_id}/ballots/{member_id}")
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json() == ballot
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Test pure proposals")
|
||||
def test_pure_proposals(network, args):
|
||||
node = network.find_random_node()
|
||||
|
||||
with node.client(None, "member0") as c:
|
||||
for prop, state in [
|
||||
(always_accept_noop, "Accepted"),
|
||||
(always_reject_noop, "Rejected"),
|
||||
]:
|
||||
r = c.post("/gov/proposals.js", prop)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == state, r.body.json()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
ballot = {"ballot": "function vote (proposal, proposer_id) { return true }"}
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 400, r.body.text()
|
||||
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/withdraw")
|
||||
assert r.status_code == 400, r.body.text()
|
||||
|
||||
return network
|
||||
|
||||
|
||||
def opposite(js_bool):
|
||||
if js_bool == "true":
|
||||
return "false"
|
||||
elif js_bool == "false":
|
||||
return "true"
|
||||
else:
|
||||
raise ValueError(f"{js_bool} is not a JavaScript boolean")
|
||||
|
||||
|
||||
@reqs.description("Test vote proposals")
|
||||
def test_proposals_with_votes(network, args):
|
||||
node = network.find_random_node()
|
||||
with node.client(None, "member0") as c:
|
||||
for prop, state, direction in [
|
||||
(always_accept_with_one_vote, "Accepted", "true"),
|
||||
(always_reject_with_one_vote, "Rejected", "false"),
|
||||
]:
|
||||
r = c.post("/gov/proposals.js", prop)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == "Open", r.body.json()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
ballot = {
|
||||
"ballot": f"function vote (proposal, proposer_id) {{ return {direction} }}"
|
||||
}
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == state, r.body.json()
|
||||
|
||||
r = c.post("/gov/proposals.js", prop)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == "Open", r.body.json()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
member_id = network.consortium.get_member_by_local_id("member0").service_id
|
||||
ballot = {
|
||||
"ballot": f'function vote (proposal, proposer_id) {{ if (proposer_id == "{member_id}") {{ return {direction} }} else {{ return {opposite(direction) } }} }}'
|
||||
}
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == state, r.body.json()
|
||||
|
||||
with node.client(None, "member0") as c:
|
||||
for prop, state, direction in [
|
||||
(always_accept_with_two_votes, "Accepted", "true"),
|
||||
(always_reject_with_two_votes, "Rejected", "false"),
|
||||
]:
|
||||
r = c.post("/gov/proposals.js", prop)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == "Open", r.body.json()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
ballot = {
|
||||
"ballot": f"function vote (proposal, proposer_id) {{ return {direction} }}"
|
||||
}
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == "Open", r.body.json()
|
||||
|
||||
with node.client(None, "member1") as oc:
|
||||
ballot = {
|
||||
"ballot": f"function vote (proposal, proposer_id) {{ return {direction} }}"
|
||||
}
|
||||
r = oc.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == state, r.body.json()
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Test operator proposals and votes")
|
||||
def test_operator_proposals_and_votes(network, args):
|
||||
node = network.find_random_node()
|
||||
with node.client(None, "member0") as c:
|
||||
r = c.post("/gov/proposals.js", always_accept_if_voted_by_operator)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == "Open", r.body.json()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
ballot = {"ballot": "function vote (proposal, proposer_id) {{ return true }}"}
|
||||
r = c.post(f"/gov/proposals.js/{proposal_id}/ballots", ballot)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == "Accepted", r.body.json()
|
||||
|
||||
with node.client(None, "member0") as c:
|
||||
r = c.post("/gov/proposals.js", always_accept_if_proposed_by_operator)
|
||||
assert r.status_code == 200, r.body.text()
|
||||
assert r.body.json()["state"] == "Accepted", r.body.json()
|
||||
proposal_id = r.body.json()["proposal_id"]
|
||||
|
||||
return network
|
||||
|
||||
|
||||
def run(args):
|
||||
with infra.network.network(
|
||||
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
||||
) as network:
|
||||
network.start_and_join(args)
|
||||
network = test_proposal_validation(network, args)
|
||||
network = test_proposal_storage(network, args)
|
||||
network = test_proposal_withdrawal(network, args)
|
||||
network = test_ballot_storage(network, args)
|
||||
network = test_pure_proposals(network, args)
|
||||
network = test_proposals_with_votes(network, args)
|
||||
network = test_operator_proposals_and_votes(network, args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = infra.e2e_args.cli_args()
|
||||
|
||||
args.package = "liblogging"
|
||||
args.nodes = ["local://localhost"]
|
||||
args.initial_user_count = 1
|
||||
run(args)
|
Загрузка…
Ссылка в новой задаче