Added smart contract upgrade support. (#250)

Supports smart contract self-upgrades by allowing "post_exec.sh" script to be executed after consensus contract execution.
This commit is contained in:
Ravin Perera
2021-02-18 17:25:42 +05:30
committed by GitHub
parent 7059f68f11
commit 8eac87fb85
6 changed files with 107 additions and 31 deletions

View File

@@ -11,13 +11,18 @@
#include <sys/stat.h>
#include "json.h"
// Private constants.
#define __HP_MMAP_BLOCK_SIZE 4096
#define __HP_MMAP_BLOCK_ALIGN(x) (((x) + ((off_t)(__HP_MMAP_BLOCK_SIZE)-1)) & ~((off_t)(__HP_MMAP_BLOCK_SIZE)-1))
#define __HP_STREAM_MSG_HEADER_SIZE 4
#define __HP_SEQPKT_MAX_SIZE 131072 // 128KB to support SEQ_PACKET sockets.
const char *__HP_PATCH_FILE_PATH = "../patch.cfg";
// Public constants.
#define HP_NPL_MSG_MAX_SIZE __HP_SEQPKT_MAX_SIZE
#define HP_KEY_SIZE 66 // Hex pubkey size. (64 char key + 2 chars for key type prfix)
#define HP_HASH_SIZE 64
const char *HP_POST_EXEC_SCRIPT_NAME = "post_exec.sh";
#define __HP_ASSIGN_STRING(dest, elem) \
{ \
@@ -174,8 +179,6 @@ struct __hp_contract
size_t user_inmap_size;
};
const char *PATCH_FILE_PATH = "../patch.cfg";
int hp_init_contract();
int hp_deinit_contract();
const struct hp_contract_context *hp_get_context();
@@ -438,7 +441,7 @@ int hp_read_npl_msg(void *msg_buf, char *pubkey_buf, const int timeout)
*/
struct hp_config *hp_get_config()
{
const int fd = open(PATCH_FILE_PATH, O_RDONLY);
const int fd = open(__HP_PATCH_FILE_PATH, O_RDONLY);
if (fd == -1)
{
fprintf(stderr, "Error opening patch.cfg file.\n");
@@ -512,7 +515,7 @@ int hp_update_config(const struct hp_config *config)
config->round_limits.proc_cpu_seconds < 0 || config->round_limits.proc_mem_bytes < 0 || config->round_limits.proc_ofd_count < 0)
__HP_UPDATE_CONFIG_ERROR("Invalid round limits.");
const int fd = open(PATCH_FILE_PATH, O_RDWR);
const int fd = open(__HP_PATCH_FILE_PATH, O_RDWR);
if (fd == -1)
__HP_UPDATE_CONFIG_ERROR("Error opening patch.cfg file.");

View File

@@ -16,6 +16,7 @@ const clientProtocols = {
Object.freeze(clientProtocols);
const PATCH_CONFIG_PATH = "../patch.cfg";
const POST_EXEC_SCRIPT_NAME = "post_exec.sh";
class HotPocketContract {
@@ -399,5 +400,6 @@ const errHandler = (err) => console.log(err);
module.exports = {
Contract: HotPocketContract,
clientProtocols
clientProtocols,
POST_EXEC_SCRIPT_NAME,
}

View File

@@ -16,6 +16,7 @@ namespace sc
constexpr int FILE_PERMS = 0644;
constexpr const char *STDOUT_LOG = ".stdout.log";
constexpr const char *STDERR_LOG = ".stderr.log";
constexpr const char *POST_EXEC_SCRIPT = "post_exec.sh";
constexpr uint32_t CONTRACT_FS_ID = 0;
@@ -51,6 +52,7 @@ namespace sc
contract_server.deinit();
contract_fs.deinit();
}
/**
* Executes the contract process and passes the specified context arguments.
* @return 0 on successful process creation. -1 on failure or contract process is already running.
@@ -61,6 +63,24 @@ namespace sc
if (start_hpfs_session(ctx) == -1)
return -1;
// Set contract working directory.
ctx.working_dir = contract_fs.physical_path(ctx.args.hpfs_session_name, STATE_DIR_PATH);
// Setup contract output log file paths.
if (conf::cfg.contract.log_output)
{
const time_t epoch = util::get_epoch_milliseconds() / 1000;
std::stringstream now_ss;
now_ss << std::put_time(std::localtime(&epoch), "%Y%m%dT%H%M%S");
const std::string now = now_ss.str();
// For consensus execution, we keep appending logs to the same out/err files.
// For read request executions, independent log files are created based on read request session names.
const std::string prefix = ctx.args.readonly ? (ctx.args.hpfs_session_name + "_" + now) : ctx.args.hpfs_session_name;
ctx.stdout_file = conf::ctx.contract_log_dir + "/" + prefix + STDOUT_LOG;
ctx.stderr_file = conf::ctx.contract_log_dir + "/" + prefix + STDERR_LOG;
}
// Create the IO sockets for users, control channel and npl.
// (Note: User socket will only be used for contract output only. For feeding user inputs we are using a memfd.)
if (create_iosockets_for_fdmap(ctx.user_fds, ctx.args.userbufs) == -1 ||
@@ -133,8 +153,7 @@ namespace sc
execv_args[j] = conf::cfg.contract.runtime_binexec_args[i].data();
execv_args[len - 1] = NULL;
const std::string current_dir = contract_fs.physical_path(ctx.args.hpfs_session_name, STATE_DIR_PATH);
chdir(current_dir.c_str());
chdir(ctx.working_dir.c_str());
if (create_contract_log_files(ctx) == -1)
{
@@ -154,6 +173,10 @@ namespace sc
cleanup_fds(ctx);
// If the consensus contact finished executing successfully, run the post-exec.sh script if it exists.
if (ctx.exit_success && !ctx.args.readonly && run_post_exec_script(ctx) == -1)
ret = -1;
if (stop_hpfs_session(ctx) == -1)
ret = -1;
@@ -214,12 +237,13 @@ namespace sc
if (WIFEXITED(scstatus))
{
ctx.exit_success = true;
LOG_DEBUG << "Contract process" << (ctx.args.readonly ? " (rdonly)" : "") << " ended normally.";
return 1;
}
else
{
LOG_ERROR << "Contract process" << (ctx.args.readonly ? " (rdonly)" : "") << " ended prematurely with code " << WEXITSTATUS(scstatus);
LOG_ERROR << "Contract process" << (ctx.args.readonly ? " (rdonly)" : "") << " ended prematurely. Exit code " << WEXITSTATUS(scstatus);
return -1;
}
}
@@ -438,6 +462,53 @@ namespace sc
LOG_DEBUG << "Contract monitor stopped";
}
/**
* Runs the contract post execution script if exists.
*/
int run_post_exec_script(const execution_context &ctx)
{
// Check whether the post-exec script exists within contract state dir.
const std::string script_path = ctx.working_dir + "/" + POST_EXEC_SCRIPT;
if (!util::is_file_exists(script_path.c_str()))
return 0;
LOG_INFO << "Running post-exec script...";
const std::string log_redirect = conf::cfg.contract.log_output ? (" >>" + ctx.stdout_file + " 2>>" + ctx.stderr_file + " ") : "";
// We set current working dir and pass lcl as command line arg to the script.
const std::string command = "(cd " + ctx.working_dir + " && ./" + POST_EXEC_SCRIPT + " " + ctx.args.lcl + log_redirect + ")";
const int ret = system(command.c_str());
if (ret == -1)
{
LOG_ERROR << errno << ": Could not run post-exec script " << script_path;
return -1;
}
else
{
// If the script returns a code 0 or 3 to 125 we consider it a successful execition.
// If the script returns code 0, we consider script lifetime is over and delete the file. Otherwise we retain the file.
const int exit_code = WEXITSTATUS(ret);
if (WIFEXITED(ret) && (exit_code == 0 || (exit_code > 2 && exit_code < 126)))
{
LOG_INFO << "Post-exec script executed successfully. Exit code:" << exit_code;
// Exit code 0 means post-execution script can be deleted.
if (exit_code == 0 && util::remove_file(script_path) == -1)
{
LOG_ERROR << errno << ": Error deleting post-exec script after execution.";
return -1;
}
}
else
{
LOG_ERROR << "Post-exec script ended prematurely. Exit code:" << exit_code;
}
return 0;
}
}
/**
* Writes any hp input messages to the contract.
*/
@@ -729,29 +800,18 @@ namespace sc
if (!conf::cfg.contract.log_output)
return 0;
const time_t epoch = util::get_epoch_milliseconds() / 1000;
std::stringstream now_ss;
now_ss << std::put_time(std::localtime(&epoch), "%Y%m%dT%H%M%S");
const std::string now = now_ss.str();
// For consensus execution, we keep appending logs to the same out/err files.
// For read request executions, independent log files are created based on read request session names.
const std::string prefix = ctx.args.readonly ? (ctx.args.hpfs_session_name + "_" + now) : ctx.args.hpfs_session_name;
const std::string stdout_file = conf::ctx.contract_log_dir + "/" + prefix + STDOUT_LOG;
const std::string stderr_file = conf::ctx.contract_log_dir + "/" + prefix + STDERR_LOG;
const int outfd = open(stdout_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS);
const int outfd = open(ctx.stdout_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS);
if (outfd == -1)
{
std::cerr << errno << ": Error opening " << stdout_file << "\n";
std::cerr << errno << ": Error opening " << ctx.stdout_file << "\n";
return -1;
}
const int errfd = open(stderr_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS);
const int errfd = open(ctx.stderr_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS);
if (errfd == -1)
{
close(outfd);
std::cerr << errno << ": Error opening " << stderr_file << "\n";
std::cerr << errno << ": Error opening " << ctx.stderr_file << "\n";
return -1;
}
@@ -759,7 +819,7 @@ namespace sc
// to mark the start of each execution.
if (!ctx.args.readonly)
{
const std::string header = "Execution lcl " + ctx.args.lcl + " on " + now + "\n";
const std::string header = "Execution lcl " + ctx.args.lcl + "\n";
if (write(outfd, header.data(), header.size()) == -1 ||
write(errfd, header.data(), header.size()) == -1)
{

View File

@@ -124,10 +124,20 @@ namespace sc
size_t total_npl_output_size = 0;
// The path set as contract working directory.
std::string working_dir;
// Full paths to std out/err log files for the contract execution.
std::string stdout_file;
std::string stderr_file;
// Indicates that the contract has sent termination control message.
bool termination_signaled = false;
// Indicates that the deinit procedure has begun.
// Indicates whether the contract exited normally without any errors.
bool exit_success = false;
// Indicates that the hpcore deinit procedure has begun.
bool is_shutting_down = false;
execution_context(util::buffer_store &user_input_store) : args(user_input_store)
@@ -158,6 +168,8 @@ namespace sc
void contract_monitor_loop(execution_context &ctx);
int run_post_exec_script(const execution_context &ctx);
int write_control_inputs(execution_context &ctx);
int write_npl_messages(execution_context &ctx);

Binary file not shown.

View File

@@ -25,7 +25,7 @@ if [ "$CONTRACT" = "cecho" ]; then # C echo contract
gcc echo_contract.c -o echo_contract
popd > /dev/null 2>&1
copyfiles="$hpcore/examples/c_contract/echo_contract"
binary="/contract/bin/echo_contract"
binary="echo_contract"
elif [ "$CONTRACT" = "nodefile" ]; then # nodejs file contract (uses BSON protocol)
echo "Using nodejs file contract."
@@ -34,13 +34,13 @@ elif [ "$CONTRACT" = "nodefile" ]; then # nodejs file contract (uses BSON protoc
popd > /dev/null 2>&1
copyfiles="$hpcore/examples/nodejs_contract/{node_modules,package.json,hp-contract-lib.js,file_contract.js}"
binary="/usr/local/bin/node"
binargs="/contract/bin/file_contract.js"
binargs="file_contract.js"
else # nodejs echo contract (default)
echo "Using nodejs echo contract."
copyfiles="$hpcore/examples/nodejs_contract/{package.json,hp-contract-lib.js,echo_contract.js}"
binary="/usr/local/bin/node"
binargs="/contract/bin/echo_contract.js"
binargs="echo_contract.js"
fi
if [ "$loglevel" = "" ]; then
@@ -123,9 +123,8 @@ do
popd > /dev/null 2>&1
# Copy the contract files and appbill.
mkdir ./node$n/bin
eval "cp -r $copyfiles ./node$n/bin/"
cp ../bin/appbill ./node$n/bin/
eval "cp -r $copyfiles ./node$n/contract_fs/seed/state/"
cp ../bin/appbill ./node$n/contract_fs/seed/state/
done
# Function to generate JSON array string while skiping a given index.