diff --git a/scripts/gen_bytecode.lua b/scripts/gen_bytecode.lua index 69709750f..802280f5c 100644 --- a/scripts/gen_bytecode.lua +++ b/scripts/gen_bytecode.lua @@ -1,5 +1,10 @@ function gen_bytecode() pay_contract = function(param) + -- 0 is a read lock, 1 is a write lock in C++ scope + Locks = { + READ = 0, + WRITE = 1, + } from, to, value, sequence, sig = string.unpack("c32 c32 I8 I8 c64", param) function get_account_key(name) @@ -8,9 +13,12 @@ function gen_bytecode() function get_account(name) account_key = get_account_key(name) - account_data = coroutine.yield(account_key) - if #account_data > 0 then - return string.unpack("I8 I8", account_data) + + account_data = coroutine.yield(account_key, Locks.WRITE) + if string.len(account_data) > 0 then + account_balance, account_sequence + = string.unpack("I8 I8", account_data) + return account_balance, account_sequence end return 0, 0 end diff --git a/src/parsec/agent/runners/interface.hpp b/src/parsec/agent/runners/interface.hpp index 9557f7257..1511e9ce0 100644 --- a/src/parsec/agent/runners/interface.hpp +++ b/src/parsec/agent/runners/interface.hpp @@ -38,7 +38,7 @@ namespace cbdc::parsec::agent::runner { internal_error, /// Function yielded more than one key to lock. yield_count, - /// Function yielded a non-string key. + /// Function yielded a invalid datatype. yield_type, /// Error acquiring lock on key. lock_error, diff --git a/src/parsec/agent/runners/lua/impl.cpp b/src/parsec/agent/runners/lua/impl.cpp index 4c79dc147..61f162eae 100644 --- a/src/parsec/agent/runners/lua/impl.cpp +++ b/src/parsec/agent/runners/lua/impl.cpp @@ -138,25 +138,56 @@ namespace cbdc::parsec::agent::runner { return buf; } + auto lua_runner::get_stack_integer(int index) -> std::optional { + if(lua_isinteger(m_state.get(), index) != 1) { + return std::nullopt; + } + return lua_tointeger(m_state.get(), index); + } + void lua_runner::schedule_contract() { int n_results{}; auto resume_ret = lua_resume(m_state.get(), nullptr, 1, &n_results); if(resume_ret == LUA_YIELD) { - if(n_results != 1) { - m_log->error("Contract yielded more than one key"); + if(n_results > 2) { + m_log->error("Contract yielded more than two keys"); + m_result_callback(error_code::yield_count); + return; + } + if(n_results < 1) { + m_log->error("Contract yielded no keys"); m_result_callback(error_code::yield_count); return; } + + auto lock_level = broker::lock_type::write; + if(n_results == 2) { + auto lock_type = get_stack_integer(-1); + if(!lock_type.has_value()) { + m_log->error("Contract yielded two keys, but the second " + "is not an integer"); + m_result_callback(error_code::yield_type); + return; + } + lua_pop(m_state.get(), 1); + + lock_level = (lock_type.value() == 0) + ? broker::lock_type::read + : broker::lock_type::write; + } + auto key_buf = get_stack_string(-1); if(!key_buf.has_value()) { m_log->error("Contract did not yield a string"); m_result_callback(error_code::yield_type); return; } - lua_pop(m_state.get(), n_results); + + lua_pop(m_state.get(), 1); + auto success = m_try_lock_callback(std::move(key_buf.value()), - broker::lock_type::write, + lock_level, [&](auto res) { handle_try_lock(std::move(res)); }); diff --git a/src/parsec/agent/runners/lua/impl.hpp b/src/parsec/agent/runners/lua/impl.hpp index 7b2c111d8..4f808fde3 100644 --- a/src/parsec/agent/runners/lua/impl.hpp +++ b/src/parsec/agent/runners/lua/impl.hpp @@ -18,6 +18,10 @@ namespace cbdc::parsec::agent::runner { /// function execution, signature checking and commiting execution results. /// Class cannot be re-used for different functions/transactions, manages /// the lifecycle of a single transaction. + /// NOTE: When writing contracts, to pass data between the Lua environment + /// and the C++ environment, use `coroutine.yield()`. To request a + /// read-lock use coroutine.yield(, 0). To request a write-lock use + /// coroutine.yield(, 1) or coroutine.yield(). class lua_runner : public interface { public: /// \copydoc interface::interface() @@ -47,6 +51,8 @@ namespace cbdc::parsec::agent::runner { auto get_stack_string(int index) -> std::optional; + auto get_stack_integer(int index) -> std::optional; + void schedule_contract(); void diff --git a/tests/unit/parsec/agent/runners/lua/read_lock_test.cpp b/tests/unit/parsec/agent/runners/lua/read_lock_test.cpp new file mode 100644 index 000000000..b3f355467 --- /dev/null +++ b/tests/unit/parsec/agent/runners/lua/read_lock_test.cpp @@ -0,0 +1,136 @@ +// Copyright (c) 2021 MIT Digital Currency Initiative, +// Federal Reserve Bank of Boston +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "../../../util.hpp" +#include "crypto/sha256.h" +#include "parsec/agent/impl.hpp" +#include "parsec/agent/runners/lua/impl.hpp" +#include "parsec/broker/impl.hpp" +#include "parsec/directory/impl.hpp" +#include "parsec/runtime_locking_shard/impl.hpp" +#include "parsec/ticket_machine/impl.hpp" +#include "parsec/util.hpp" +#include "util/common/keys.hpp" +#include "util/serialization/buffer_serializer.hpp" +#include "util/serialization/format.hpp" + +#include +#include +#include +#include +#include +#include + +TEST(lua_runner_test, lua_write_lock_test) { + auto log = std::make_shared( + cbdc::logging::log_level::trace); + lua_State* L = luaL_newstate(); + luaL_openlibs(L); + luaL_dofile(L, + "../tests/unit/parsec/agent/runners/lua/test_write_locks.lua"); + lua_getglobal(L, "gen_bytecode"); + ASSERT_EQ(lua_pcall(L, 0, 1, 0), 0); + auto contract = cbdc::buffer::from_hex(lua_tostring(L, -1)).value(); + auto func = cbdc::buffer::from_hex(contract.to_hex()).value(); + auto param = cbdc::buffer(); + auto cfg = cbdc::parsec::config(); + + auto result_cb = [&](cbdc::parsec::agent::runner::interface:: + run_return_type /* value */) { + return; + }; + + std::promise write_lock_promise; + auto write_lock_future = write_lock_promise.get_future(); + auto try_lock_cb + = [&](const cbdc::parsec::broker::key_type& key, + cbdc::parsec::broker::lock_type locktype, + const cbdc::parsec::broker::interface::try_lock_callback_type& + /* res_cb */) -> bool { + // Cannot use ASSERT here because it does not satisfy return + // requirements so we use expect + if(std::string("W").compare(key.c_str()) == 0) { + EXPECT_TRUE(locktype == cbdc::parsec::broker::lock_type::write); + write_lock_promise.set_value(); + } else { + // Cannot use FAIL() here because it expands to a return statement + // which does not satisfy the requirements of a try_lock_callback + // so we improvise + EXPECT_FALSE(true); + } + return true; + }; + + auto runner + = cbdc::parsec::agent::runner::lua_runner(log, + cfg, + std::move(func), + std::move(param), + false, + std::move(result_cb), + std::move(try_lock_cb), + nullptr, + nullptr, + 0); + auto res = runner.run(); + write_lock_future.get(); + ASSERT_TRUE(res); +} + +TEST(lua_runner_test, lua_read_lock_test) { + auto log = std::make_shared( + cbdc::logging::log_level::trace); + lua_State* L = luaL_newstate(); + luaL_openlibs(L); + luaL_dofile(L, + "../tests/unit/parsec/agent/runners/lua/test_read_locks.lua"); + lua_getglobal(L, "gen_bytecode"); + ASSERT_EQ(lua_pcall(L, 0, 1, 0), 0); + auto contract = cbdc::buffer::from_hex(lua_tostring(L, -1)).value(); + auto func = cbdc::buffer::from_hex(contract.to_hex()).value(); + auto param = cbdc::buffer(); + auto cfg = cbdc::parsec::config(); + + auto result_cb = [&](cbdc::parsec::agent::runner::interface:: + run_return_type /* value */) { + return; + }; + + std::promise read_lock_promise; + auto read_lock_future = read_lock_promise.get_future(); + auto try_lock_cb + = [&](const cbdc::parsec::broker::key_type& key, + cbdc::parsec::broker::lock_type locktype, + const cbdc::parsec::broker::interface::try_lock_callback_type& + /* res_cb */) -> bool { + // Cannot use ASSERT here because it does not satisfy return + // requirements so we use expect + if(std::string("R").compare(key.c_str()) == 0) { + EXPECT_TRUE(locktype == cbdc::parsec::broker::lock_type::read); + read_lock_promise.set_value(); + } else { + // Cannot use FAIL() here because it expands to a return statement + // which does not satisfy the requirements of a try_lock_callback + // so we improvise + EXPECT_FALSE(true); + } + return true; + }; + + auto runner + = cbdc::parsec::agent::runner::lua_runner(log, + cfg, + std::move(func), + std::move(param), + false, + std::move(result_cb), + std::move(try_lock_cb), + nullptr, + nullptr, + 0); + auto res = runner.run(); + read_lock_future.get(); + ASSERT_TRUE(res); +} diff --git a/tests/unit/parsec/agent/runners/lua/test_read_locks.lua b/tests/unit/parsec/agent/runners/lua/test_read_locks.lua new file mode 100644 index 000000000..cbb34642d --- /dev/null +++ b/tests/unit/parsec/agent/runners/lua/test_read_locks.lua @@ -0,0 +1,17 @@ +function gen_bytecode() + pay_contract = function(param) + Locks = { + READ = 0, + WRITE = 1, + } + data = coroutine.yield("R", Locks.READ) + return "Done" + end + c = string.dump(pay_contract, true) + t = {} + for i = 1, #c do + t[#t + 1] = string.format("%02x", string.byte(c, i)) + end + + return table.concat(t) +end diff --git a/tests/unit/parsec/agent/runners/lua/test_write_locks.lua b/tests/unit/parsec/agent/runners/lua/test_write_locks.lua new file mode 100644 index 000000000..5ad423256 --- /dev/null +++ b/tests/unit/parsec/agent/runners/lua/test_write_locks.lua @@ -0,0 +1,17 @@ +function gen_bytecode() + pay_contract = function(param) + Locks = { + READ = 0, + WRITE = 1, + } + data2 = coroutine.yield("W", Locks.WRITE) + return "Done" + end + c = string.dump(pay_contract, true) + t = {} + for i = 1, #c do + t[#t + 1] = string.format("%02x", string.byte(c, i)) + end + + return table.concat(t) +end diff --git a/tests/unit/parsec/agent/runners/lua/write_lock_test.cpp b/tests/unit/parsec/agent/runners/lua/write_lock_test.cpp new file mode 100644 index 000000000..130ae52bd --- /dev/null +++ b/tests/unit/parsec/agent/runners/lua/write_lock_test.cpp @@ -0,0 +1,93 @@ +// Copyright (c) 2021 MIT Digital Currency Initiative, +// Federal Reserve Bank of Boston +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "../../../util.hpp" +#include "crypto/sha256.h" +#include "parsec/agent/impl.hpp" +#include "parsec/agent/runners/lua/impl.hpp" +#include "parsec/broker/impl.hpp" +#include "parsec/directory/impl.hpp" +#include "parsec/runtime_locking_shard/impl.hpp" +#include "parsec/ticket_machine/impl.hpp" +#include "parsec/util.hpp" +#include "util/common/keys.hpp" +#include "util/serialization/buffer_serializer.hpp" +#include "util/serialization/format.hpp" + +#include +#include +#include +#include +#include +#include + +TEST(lua_runner_test, lua_read_lock_test) { + auto log = std::make_shared( + cbdc::logging::log_level::trace); + lua_State* L = luaL_newstate(); + luaL_openlibs(L); + luaL_dofile(L, + "../tests/unit/parsec/agent/runners/lua/test_write_locks.lua"); + lua_getglobal(L, "gen_bytecode"); + ASSERT_EQ(lua_pcall(L, 0, 1, 0), 0); + auto contract = cbdc::buffer::from_hex(lua_tostring(L, -1)).value(); + auto func = cbdc::buffer::from_hex(contract.to_hex()).value(); + auto param = cbdc::buffer(); + auto cfg = cbdc::parsec::config(); + + auto result_cb = [&](cbdc::parsec::agent::runner::interface:: + run_return_type /* value */) { + return; + }; + + std::atomic lock_count = 0; + std::promise write_lock_promise; + std::promise read_lock_promise; + auto write_lock_future = write_lock_promise.get_future(); + auto read_lock_future = read_lock_promise.get_future(); + auto try_lock_cb + = [&](const cbdc::parsec::broker::key_type& key, + cbdc::parsec::broker::lock_type locktype, + const cbdc::parsec::broker::interface::try_lock_callback_type& + /* res_cb */) -> bool { + // Cannot use ASSERT here because it does not satisfy return + // requirements so we use expect + log->trace("Got lock for key:", key.c_str()); + if(std::string("W").compare(key.c_str()) == 0) { + log->trace("Got write lock"); + EXPECT_TRUE(locktype == cbdc::parsec::broker::lock_type::write); + lock_count++; + write_lock_promise.set_value(); + } else if(std::string("R").compare(key.c_str()) == 0) { + log->trace("Got read lock"); + EXPECT_TRUE(locktype == cbdc::parsec::broker::lock_type::read); + lock_count++; + read_lock_promise.set_value(); + } else { + // Cannot use FAIL() here because it expands to a return statement + // which does not satisfy the requirements of a try_lock_callback + // so we improvise + EXPECT_FALSE(true); + } + return true; + }; + + auto runner + = cbdc::parsec::agent::runner::lua_runner(log, + cfg, + std::move(func), + std::move(param), + false, + std::move(result_cb), + std::move(try_lock_cb), + nullptr, + nullptr, + 0); + auto res = runner.run(); + write_lock_future.get(); + read_lock_future.get(); + ASSERT_EQ(lock_count, 2); + ASSERT_TRUE(res); +}