Skip to content

Historical JS endpoints #2285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Mar 15, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Historical point query support has been added to JavaScript endpoints (#2285).

## [0.19.0]

### Changed
Expand Down
10 changes: 7 additions & 3 deletions cmake/quickjs.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ if("sgx" IN_LIST COMPILE_TARGETS)
quickjs.enclave STATIC ${QUICKJS_SRC} ${CCF_DIR}/3rdparty/stub/time.c
)
target_compile_options(
quickjs.enclave PUBLIC -nostdinc -DCONFIG_VERSION="${QUICKJS_VERSION}"
-DEMSCRIPTEN -DCONFIG_STACK_CHECK
quickjs.enclave
PUBLIC -nostdinc -DCONFIG_VERSION="${QUICKJS_VERSION}" -DEMSCRIPTEN
-DCONFIG_STACK_CHECK
PRIVATE $<$<CONFIG:Debug>:-DDUMP_LEAKS>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check, this is saying "when building this target in Debug, set DUMP_LEAKS", right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah

)
target_link_libraries(quickjs.enclave PUBLIC ${OE_TARGET_LIBC})
set_property(TARGET quickjs.enclave PROPERTY POSITION_INDEPENDENT_CODE ON)
Expand All @@ -40,7 +42,9 @@ endif()

add_library(quickjs.host STATIC ${QUICKJS_SRC})
target_compile_options(
quickjs.host PUBLIC -DCONFIG_VERSION="${QUICKJS_VERSION}"
quickjs.host
PUBLIC -DCONFIG_VERSION="${QUICKJS_VERSION}"
PRIVATE $<$<CONFIG:Debug>:-DDUMP_LEAKS>
)
add_san(quickjs.host)
set_property(TARGET quickjs.host PROPERTY POSITION_INDEPENDENT_CODE ON)
Expand Down
26 changes: 26 additions & 0 deletions samples/apps/forum/src/types/ccf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ export interface KVMap {

export type KVMaps = { [key: string]: KVMap };

export interface ProofElement {
left?: string;
right?: string;
}

export type Proof = ProofElement[];

export interface Receipt {
signature: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a jsdoc comments like /** base64-encoded signature of the root by the node identified by nodeId */, and if we did, would it get picked up by sphinx-js? Just wondering, not requesting :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and yes, see the CryptoKeyPair interface in #2313

root: string;
proof: Proof;
leaf: string;
nodeId: string;
}

export interface HistoricalState {
transactionId: string;
receipt: Receipt;
}

interface WrapAlgoParams {
name: string;
}
Expand All @@ -61,6 +81,11 @@ export interface AESKWPParams extends WrapAlgoParams {
name: "AES-KWP";
}

export interface RsaOaepAESKWPParams extends WrapAlgoParams {
name: "RSA-OAEP-AES-KWP";
label?: ArrayBuffer;
}

export interface CryptoKeyPair {
privateKey: string;
publicKey: string;
Expand All @@ -80,6 +105,7 @@ export interface CCF {
): ArrayBuffer;

kv: KVMaps;
historicalState?: HistoricalState;
}

export const ccf = globalThis.ccf as CCF;
Expand Down
24 changes: 24 additions & 0 deletions samples/apps/logging/js/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@
"openapi": {}
}
},
"/log/private/historical": {
"get": {
"js_module": "logging.js",
"js_function": "get_historical",
"forwarding_required": "never",
"execute_outside_consensus": "never",
"authn_policies": ["jwt", "user_cert"],
"historical": true,
"readonly": true,
"openapi": {}
}
},
"/log/private/historical_receipt": {
"get": {
"js_module": "logging.js",
"js_function": "get_historical_with_receipt",
"forwarding_required": "never",
"execute_outside_consensus": "never",
"authn_policies": ["jwt", "user_cert"],
"historical": true,
"readonly": true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Historical implies readonly, it would be elegant if the config reflected that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"mode": "readonly|writable|historical" ? Or were you thinking of something different?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that'd be good. Paging @eddyashton for API input, but I think this would have the merit of ruling out historical: true, readonly: false and the validation and error that'd need to go with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this is a tri-state enum. I like readonly and historical but not convinced by writable. I think readwrite is what we've used elsewhere (eg tx.rw)? I could also go for canwrite or maywrite or just write, but I think readwrite is best.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readwrite is fine. I'll do the change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@achamayou @eddyashton Done, please have a final look if the change looks good.

"openapi": {}
}
},
"/log/public": {
"get": {
"js_module": "logging.js",
Expand Down
10 changes: 10 additions & 0 deletions samples/apps/logging/js/src/logging.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ export function get_private(request) {
return get_record(ccf.kv["records"], id);
}

export function get_historical(request) {
return get_private(request);
}

export function get_historical_with_receipt(request) {
const result = get_private(request);
result.body.receipt = ccf.historicalState.receipt;
return result;
}

export function get_public(request) {
const id = get_id_from_request_query(request);
return get_record(ccf.kv["public:records"], id);
Expand Down
26 changes: 2 additions & 24 deletions samples/apps/logging/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -546,30 +546,8 @@ namespace loggingapp
kv::Consensus::View view,
kv::Consensus::SeqNo seqno,
std::string& error_reason) {
if (consensus == nullptr)
{
error_reason = "Node is not fully configured";
return false;
}

const auto tx_view = consensus->get_view(seqno);
const auto committed_seqno = consensus->get_committed_seqno();
const auto committed_view = consensus->get_view(committed_seqno);

const auto tx_status = ccf::evaluate_tx_status(
view, seqno, tx_view, committed_view, committed_seqno);
if (tx_status != ccf::TxStatus::Committed)
{
error_reason = fmt::format(
"Only committed transactions can be queried. Transaction {}.{} is "
"{}",
view,
seqno,
ccf::tx_status_to_str(tx_status));
return false;
}

return true;
return ccf::historical::is_tx_committed(
consensus, view, seqno, error_reason);
};
make_endpoint(
"log/private/historical",
Expand Down
123 changes: 116 additions & 7 deletions src/apps/js_generic/js_generic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,9 @@ namespace ccfapp
JSValueConst this_val,
JSAtom property)
{
const auto property_name = JS_AtomToCString(ctx, property);
const auto property_name_c = JS_AtomToCString(ctx, property);
const std::string property_name(property_name_c);
JS_FreeCString(ctx, property_name_c);
LOG_TRACE_FMT("Looking for kv map '{}'", property_name);

const auto [security_domain, access_category] =
Expand Down Expand Up @@ -807,6 +809,7 @@ namespace ccfapp
{
private:
NetworkTables& network;
ccfapp::AbstractNodeContext& context;

JSClassDef kv_class_def = {};
JSClassExoticMethods kv_exotic_methods = {};
Expand All @@ -817,7 +820,11 @@ namespace ccfapp

metrics::Tracker metrics_tracker;

static JSValue create_ccf_obj(EndpointContext& args, JSContext* ctx)
static JSValue create_ccf_obj(
kv::Tx& tx,
const std::optional<kv::TxID>& transaction_id,
historical::TxReceiptPtr receipt,
JSContext* ctx)
{
auto ccf = JS_NewObject(ctx);

Expand Down Expand Up @@ -861,9 +868,62 @@ namespace ccfapp
JS_NewCFunction(ctx, ccfapp::js_wrap_key, "wrapKey", 3));

auto kv = JS_NewObjectClass(ctx, kv_class_id);
JS_SetOpaque(kv, &args.tx);
JS_SetOpaque(kv, &tx);
JS_SetPropertyStr(ctx, ccf, "kv", kv);

// Historical queries
if (receipt)
{
auto state = JS_NewObject(ctx);

ccf::TxID tx_id;
tx_id.seqno = static_cast<ccf::SeqNo>(transaction_id.value().version);
tx_id.view = static_cast<ccf::View>(transaction_id.value().term);
JS_SetPropertyStr(
ctx,
state,
"transactionId",
JS_NewString(ctx, tx_id.to_str().c_str()));

ccf::GetReceipt::Out receipt_out;
receipt_out.from_receipt(receipt);
auto js_receipt = JS_NewObject(ctx);
JS_SetPropertyStr(
ctx,
js_receipt,
"signature",
JS_NewString(ctx, receipt_out.signature.c_str()));
JS_SetPropertyStr(
ctx, js_receipt, "root", JS_NewString(ctx, receipt_out.root.c_str()));
JS_SetPropertyStr(
ctx, js_receipt, "leaf", JS_NewString(ctx, receipt_out.leaf.c_str()));
JS_SetPropertyStr(
ctx,
js_receipt,
"nodeId",
JS_NewString(ctx, receipt_out.node_id.value().c_str()));
auto proof = JS_NewArray(ctx);
uint32_t i = 0;
for (auto& element : receipt_out.proof)
{
auto js_element = JS_NewObject(ctx);
auto is_left = element.left.has_value();
JS_SetPropertyStr(
ctx,
js_element,
is_left ? "left" : "right",
JS_NewString(
ctx, (is_left ? element.left : element.right).value().c_str()));
JS_DefinePropertyValueUint32(
ctx, proof, i++, js_element, JS_PROP_C_W_E);
}
JS_SetPropertyStr(ctx, js_receipt, "proof", proof);

JS_SetPropertyStr(ctx, state, "receipt", js_receipt);

JS_SetPropertyStr(ctx, ccf, "historicalState", state);
}

return ccf;
}

Expand All @@ -877,12 +937,20 @@ namespace ccfapp
return console;
}

static void populate_global_obj(EndpointContext& args, JSContext* ctx)
static void populate_global_obj(
kv::Tx& tx,
const std::optional<kv::TxID>& transaction_id,
ccf::historical::TxReceiptPtr receipt,
JSContext* ctx)
{
auto global_obj = JS_GetGlobalObject(ctx);

JS_SetPropertyStr(ctx, global_obj, "console", create_console_obj(ctx));
JS_SetPropertyStr(ctx, global_obj, "ccf", create_ccf_obj(args, ctx));
JS_SetPropertyStr(
ctx,
global_obj,
"ccf",
create_ccf_obj(tx, transaction_id, receipt, ctx));

JS_FreeValue(ctx, global_obj);
}
Expand Down Expand Up @@ -1043,6 +1111,46 @@ namespace ccfapp
const std::string& method,
const ccf::RESTVerb& verb,
EndpointContext& args)
{
// Is this a historical endpoint?
auto endpoints =
args.tx.ro<ccf::endpoints::EndpointsMap>(ccf::Tables::ENDPOINTS);
auto info = endpoints->get(ccf::endpoints::EndpointKey{method, verb});

if (info.has_value() && info.value().historical)
{
auto is_tx_committed = [this](
kv::Consensus::View view,
kv::Consensus::SeqNo seqno,
std::string& error_reason) {
return ccf::historical::is_tx_committed(
consensus, view, seqno, error_reason);
};

ccf::historical::adapter(
[this, &method, &verb](
ccf::EndpointContext& args, ccf::historical::StatePtr state) {
auto tx = state->store->create_tx();
auto tx_id = state->transaction_id;
auto receipt = state->receipt;
do_execute_request(method, verb, args, tx, tx_id, receipt);
},
context.get_historical_state(),
is_tx_committed)(args);
}
else
{
do_execute_request(method, verb, args, args.tx, std::nullopt, nullptr);
}
}

void do_execute_request(
const std::string& method,
const ccf::RESTVerb& verb,
EndpointContext& args,
kv::Tx& target_tx,
const std::optional<kv::TxID>& transaction_id,
ccf::historical::TxReceiptPtr receipt)
{
const auto local_method = method.substr(method.find_first_not_of('/'));

Expand Down Expand Up @@ -1130,7 +1238,7 @@ namespace ccfapp
JS_SetClassProto(ctx, body_class_id, body_proto);

// Populate globalThis with console and ccf globals
populate_global_obj(args, ctx);
populate_global_obj(target_tx, transaction_id, receipt, ctx);

// Compile module
if (!handler_script.value().text.has_value())
Expand Down Expand Up @@ -1356,7 +1464,8 @@ namespace ccfapp
public:
JSHandlers(NetworkTables& network, AbstractNodeContext& context) :
UserEndpointRegistry(context),
network(network)
network(network),
context(context)
{
JS_NewClassID(&kv_class_id);
kv_exotic_methods.get_own_property = js_kv_lookup;
Expand Down
33 changes: 33 additions & 0 deletions src/node/historical_queries_adapter.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@ namespace ccf::historical
return tx_id_opt;
}

static inline bool is_tx_committed(
kv::Consensus* consensus,
kv::Consensus::View view,
kv::Consensus::SeqNo seqno,
std::string& error_reason)
{
if (consensus == nullptr)
{
error_reason = "Node is not fully configured";
return false;
}

const auto tx_view = consensus->get_view(seqno);
const auto committed_seqno = consensus->get_committed_seqno();
const auto committed_view = consensus->get_view(committed_seqno);

const auto tx_status = ccf::evaluate_tx_status(
view, seqno, tx_view, committed_view, committed_seqno);
if (tx_status != ccf::TxStatus::Committed)
{
error_reason = fmt::format(
"Only committed transactions can be queried. Transaction {}.{} "
"is "
"{}",
view,
seqno,
ccf::tx_status_to_str(tx_status));
return false;
}

return true;
};

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"

Expand Down
Loading