зеркало из https://github.com/microsoft/CCF.git
No-op JS gov (#2350)
This commit is contained in:
Родитель
7af1179111
Коммит
e3364b7200
|
@ -27,7 +27,7 @@ parameters:
|
||||||
perf:
|
perf:
|
||||||
cmake_args: '-DBUILD_UNIT_TESTS=OFF -DDISTRIBUTE_PERF_TESTS="`../.nodes.sh`"'
|
cmake_args: '-DBUILD_UNIT_TESTS=OFF -DDISTRIBUTE_PERF_TESTS="`../.nodes.sh`"'
|
||||||
release:
|
release:
|
||||||
cmake_args: "-DTLS_TEST=ON -DENABLE_BFT=OFF"
|
cmake_args: "-DTLS_TEST=ON -DENABLE_BFT=OFF -DENABLE_JS_GOV=OFF"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
NoSGX:
|
NoSGX:
|
||||||
|
|
|
@ -60,6 +60,10 @@ option(ZAP_TEST
|
||||||
"ZAP fuzz test using https://www.zaproxy.org/docs/docker/api-scan/" OFF
|
"ZAP fuzz test using https://www.zaproxy.org/docs/docker/api-scan/" OFF
|
||||||
)
|
)
|
||||||
option(BUILD_SMALLBANK "Build SmallBank sample app and clients" ON)
|
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)
|
option(BUILD_TPCC "Build TPPC sample app and clients" ON)
|
||||||
|
|
||||||
# Build common library for CCF enclaves
|
# 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(hash_bench SRCS src/ds/test/hash_bench.cpp)
|
||||||
add_picobench(digest_bench SRCS src/crypto/test/digest_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
|
# Storing signed governance operations
|
||||||
add_e2e_test(
|
add_e2e_test(
|
||||||
NAME governance_history_test
|
NAME governance_history_test
|
||||||
|
|
|
@ -1,6 +1,38 @@
|
||||||
{
|
{
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"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": {
|
"CodeStatus": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"AllowedToJoin"
|
"AllowedToJoin"
|
||||||
|
@ -60,24 +92,6 @@
|
||||||
"pattern": "^[a-f0-9]{64}$",
|
"pattern": "^[a-f0-9]{64}$",
|
||||||
"type": "string"
|
"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": {
|
"GetCode__Out": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"versions": {
|
"versions": {
|
||||||
|
@ -164,28 +178,12 @@
|
||||||
},
|
},
|
||||||
"Proposal": {
|
"Proposal": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"parameter": {
|
"actions": {
|
||||||
"$ref": "#/components/schemas/json"
|
"$ref": "#/components/schemas/Action_array"
|
||||||
},
|
|
||||||
"proposer": {
|
|
||||||
"$ref": "#/components/schemas/EntityId"
|
|
||||||
},
|
|
||||||
"script": {
|
|
||||||
"$ref": "#/components/schemas/Script"
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"$ref": "#/components/schemas/ProposalState"
|
|
||||||
},
|
|
||||||
"votes": {
|
|
||||||
"$ref": "#/components/schemas/EntityId_to_Script"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"script",
|
"actions"
|
||||||
"parameter",
|
|
||||||
"proposer",
|
|
||||||
"state",
|
|
||||||
"votes"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
@ -208,6 +206,29 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"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": {
|
"ProposalState": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"Open",
|
"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}": {
|
"/proposals/{proposal_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|
|
@ -536,7 +536,6 @@ namespace ccfapp
|
||||||
network(network),
|
network(network),
|
||||||
context(context)
|
context(context)
|
||||||
{
|
{
|
||||||
js::register_class_ids();
|
|
||||||
metrics_tracker.install_endpoint(*this);
|
metrics_tracker.install_endpoint(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -523,7 +523,7 @@ namespace js
|
||||||
}
|
}
|
||||||
|
|
||||||
// Historical queries
|
// Historical queries
|
||||||
if (receipt)
|
if (receipt != nullptr)
|
||||||
{
|
{
|
||||||
auto state = JS_NewObject(ctx);
|
auto state = JS_NewObject(ctx);
|
||||||
|
|
||||||
|
|
|
@ -5,26 +5,69 @@
|
||||||
#include "ds/json.h"
|
#include "ds/json.h"
|
||||||
#include "entities.h"
|
#include "entities.h"
|
||||||
#include "kv/map.h"
|
#include "kv/map.h"
|
||||||
|
#include "proposals.h"
|
||||||
|
|
||||||
namespace ccf
|
namespace ccf
|
||||||
{
|
{
|
||||||
namespace jsgov
|
namespace jsgov
|
||||||
{
|
{
|
||||||
using ProposalId = std::string;
|
using ProposalId = std::string;
|
||||||
using Proposal = std::string;
|
|
||||||
struct ProposalInfo
|
struct ProposalInfo
|
||||||
{
|
{
|
||||||
ccf::MemberId proposer_id;
|
ccf::MemberId proposer_id;
|
||||||
|
ccf::ProposalState state;
|
||||||
std::unordered_map<ccf::MemberId, std::string> ballots = {};
|
std::unordered_map<ccf::MemberId, std::string> ballots = {};
|
||||||
};
|
};
|
||||||
DECLARE_JSON_TYPE(ProposalInfo)
|
DECLARE_JSON_TYPE(ProposalInfo);
|
||||||
DECLARE_JSON_REQUIRED_FIELDS(ProposalInfo, proposer_id, ballots);
|
DECLARE_JSON_REQUIRED_FIELDS(ProposalInfo, proposer_id, state, ballots);
|
||||||
|
|
||||||
using ProposalMap = kv::RawCopySerialisedMap<ProposalId, Proposal>;
|
struct ProposalInfoSummary
|
||||||
using ProposalInfoMap = kv::MapSerialisedWith<
|
{
|
||||||
ProposalId,
|
ProposalId proposal_id;
|
||||||
ProposalInfo,
|
ccf::MemberId proposer_id;
|
||||||
kv::serialisers::BlitSerialiser,
|
ccf::ProposalState state;
|
||||||
kv::serialisers::JsonSerialiser>;
|
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 "genesis_gen.h"
|
||||||
#include "history.h"
|
#include "history.h"
|
||||||
#include "hooks.h"
|
#include "hooks.h"
|
||||||
|
#include "js/wrap.h"
|
||||||
#include "network_state.h"
|
#include "network_state.h"
|
||||||
#include "node/jwt_key_auto_refresh.h"
|
#include "node/jwt_key_auto_refresh.h"
|
||||||
#include "node/progress_tracker.h"
|
#include "node/progress_tracker.h"
|
||||||
|
@ -343,6 +344,7 @@ namespace ccf
|
||||||
|
|
||||||
config = std::move(config_);
|
config = std::move(config_);
|
||||||
|
|
||||||
|
js::register_class_ids();
|
||||||
open_frontend(ActorsType::nodes);
|
open_frontend(ActorsType::nodes);
|
||||||
|
|
||||||
#ifdef GET_QUOTE
|
#ifdef GET_QUOTE
|
||||||
|
|
|
@ -482,7 +482,12 @@ namespace ccf
|
||||||
|
|
||||||
void set_root_on_proposals(const enclave::RpcContext& ctx, kv::Tx& tx)
|
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();
|
update_history();
|
||||||
if (history)
|
if (history)
|
||||||
|
|
|
@ -1152,6 +1152,152 @@ namespace ccf
|
||||||
return get_proposal_info(proposal_id, proposal);
|
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)
|
bool check_member_active(kv::ReadOnlyTx& tx, const MemberId& id)
|
||||||
{
|
{
|
||||||
return check_member_status(tx, id, {MemberStatus::ACTIVE});
|
return check_member_status(tx, id, {MemberStatus::ACTIVE});
|
||||||
|
@ -2110,6 +2256,542 @@ namespace ccf
|
||||||
{std::make_shared<NodeCertAuthnPolicy>()})
|
{std::make_shared<NodeCertAuthnPolicy>()})
|
||||||
.set_openapi_hidden(true)
|
.set_openapi_hidden(true)
|
||||||
.install();
|
.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)
|
Загрузка…
Ссылка в новой задаче