diff --git a/CMakeLists.txt b/CMakeLists.txt index de160e2d..1818023b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,12 +123,19 @@ target_link_libraries(hpstatemon libfuse3.so.3 ) +add_executable(appbill + src/bill/appbill.cpp +) + add_dependencies(hpcore hpstatemon + appbill ) + # add_custom_command(TARGET hpcore POST_BUILD # COMMAND strip ./build/hpcore # COMMAND strip ./build/hpstatemon +# COMMAND strip ./build/appbill # ) target_precompile_headers(hpsupport PUBLIC src/pchheader.hpp) diff --git a/cluster-create.sh b/cluster-create.sh index 539b87ee..a03112f5 100755 --- a/cluster-create.sh +++ b/cluster-create.sh @@ -48,6 +48,8 @@ do node -p "JSON.stringify({...require('./tmp.json'), \ binary: '/usr/local/bin/node', \ binargs: './bin/contract.js', \ + appbill: 'appbill', \ + appbillargs: '', \ peerport: ${peerport}, \ pubport: ${pubport}, \ roundtime: 1000, \ @@ -61,9 +63,10 @@ do -subj "/C=AU/ST=ST/L=L/O=O/OU=OU/CN=localhost/emailAddress=hpnode${n}@example" > /dev/null 2>&1 popd > /dev/null 2>&1 - # Copy the contract executable. + # Copy the contract executable and appbill. mkdir ./node$n/bin cp ../examples/echocontract/contract.js ./node$n/bin/contract.js + cp ../build/appbill ./node$n/bin/ done # Function to generate JSON array string while skiping a given index. @@ -107,6 +110,23 @@ do popd > /dev/null 2>&1 done +# Setup initial state data for all nodes but one. +for (( i=1; i<=$ncount; i++ )) +do + + sudo mkdir -p ./node$i/statehist/0/data/ > /dev/null 2>&1 + + # Load credit balance for user for testing purposes. + pushd ./node$i/statehist/0/data/ > /dev/null 2>&1 + >appbill.table + ../../../../../build/appbill --credit "705bf26354ee4c63c0e5d5d883c07cefc3196d049bd3825f827eb3bc23ead035" 10000 + popd > /dev/null 2>&1 + + # Copy any more initial state files for testing. + #cp ~/my_big_file ~/hpcore/hpcluster/node$i/statehist/0/data/ + +done + popd > /dev/null 2>&1 # Create docker virtual network named 'hpnet' @@ -116,4 +136,4 @@ docker network create --driver bridge hpnet > /dev/null 2>&1 echo "Cluster generated at ${clusterloc}" echo "Use \"./cluster-start.sh \" to run each node." -exit 0 \ No newline at end of file +exit 0 diff --git a/examples/echocontract/contract.js b/examples/echocontract/contract.js index abe94886..a2154485 100644 --- a/examples/echocontract/contract.js +++ b/examples/echocontract/contract.js @@ -6,7 +6,7 @@ const fs = require('fs') //console.log("===Sample contract started==="); //console.log("Contract args received from hp: " + input); -let hpargs = JSON.parse( fs.readFileSync(0, 'utf8')); +let hpargs = JSON.parse(fs.readFileSync(0, 'utf8')); // We just save execution args as an example state file change. fs.appendFileSync("state/exects.txt", "ts:" + hpargs.ts + "\n"); @@ -36,4 +36,4 @@ if (hpinput.length > 0) { fs.writeSync(hpargs.hpfd[1], "Echoing: " + hpinput); } -//console.log("===Sample contract ended==="); \ No newline at end of file +//console.log("===Sample contract ended==="); diff --git a/examples/hpclient/.gitignore b/examples/hpclient/.gitignore index 0a3fc7dd..394522f4 100644 --- a/examples/hpclient/.gitignore +++ b/examples/hpclient/.gitignore @@ -1,2 +1 @@ -node_modules/** -.hp_client_keys \ No newline at end of file +node_modules/** \ No newline at end of file diff --git a/examples/hpclient/.hp_client_keys b/examples/hpclient/.hp_client_keys new file mode 100644 index 00000000..b09c0fde --- /dev/null +++ b/examples/hpclient/.hp_client_keys @@ -0,0 +1 @@ +{"publicKey":"705bf26354ee4c63c0e5d5d883c07cefc3196d049bd3825f827eb3bc23ead035","privateKey":"07ce6f7d6f0da38d5956ecc4ea1d18de77fc8bded089eb52199a46ffe2098c88705bf26354ee4c63c0e5d5d883c07cefc3196d049bd3825f827eb3bc23ead035","keyType":"ed25519"} \ No newline at end of file diff --git a/reset.sh b/reset.sh index 8eb87bf6..023edd36 100755 --- a/reset.sh +++ b/reset.sh @@ -2,12 +2,16 @@ nodes=3 sudo ./cluster-create.sh $nodes - +WD=`pwd` # Setup initial state data for all nodes but one. for (( i=1; i<$nodes; i++ )) do - + sudo mkdir -p ~/hpcore/hpcluster/node$i/statehist/0/data/ + pushd ~/hpcore/hpcluster/node$i/statehist/0/data/ + >appbill.table + $WD/build/appbill --credit 705bf26354ee4c63c0e5d5d883c07cefc3196d049bd3825f827eb3bc23ead035 10000 + popd #sudo cp -r ~/Downloads/big.mkv ~/hpcore/hpcluster/node$i/statehist/0/data/ -done \ No newline at end of file +done diff --git a/src/bill/appbill.cpp b/src/bill/appbill.cpp new file mode 100644 index 00000000..66c19cfb --- /dev/null +++ b/src/bill/appbill.cpp @@ -0,0 +1,705 @@ +/* + App bill default implementation 1 + MSB of return value is reserved for appbill error + bits 1-8 indicate whether or not public keys 0-6 passed appbill check + + (to be completed) +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG 0 +#define KEY_SIZE 32 +#define RECORD_SIZE 64 + +#define FILE_BUFFER_SIZE (64*1024*1024) // this will move 0xffff entries at a time + +#define MIN(a,b) (((a)<(b))?(a):(b)) +#define MAX(a,b) (((a)>(b))?(a):(b)) + +#define TABLE_FILE "appbill.table" +#define TABLE_FILE_2 "./state/appbill.table" // if TABLE_FILE can't be found try here + +uint64_t new_balance(uint64_t balance, int64_t to_credit) { + if (to_credit < 0 && -to_credit > balance) { + // catch the wrap around + balance = 0; + } else if (to_credit > 0 && to_credit + balance < balance) { + // and here as well + balance = (uint64_t)-1; + } else { + // normal crediting + balance += to_credit; + } + return balance; +} + +void print_hex(uint8_t* data, int len) { + for (int c = 0; c < len; ++c) + printf("%02hhx", data[c]); +} + +int compar (const void* p1, const void* p2) { + for (uint8_t* c1 = (uint8_t*)p1, * c2 = (uint8_t*)p2; c1 - (uint8_t*)p1 < KEY_SIZE; ++c1, ++c2) + if (*c1 < *c2) return -1; + else if (*c1 > *c2) return +1; + return 0; +} + + +int correct_for_ed_keys(int argc, char** argv, int incr, int offset) { + // correct for ed keys + for (int i = 0; i < argc; i += incr) { + int len = strlen(argv[i + offset]); + if (len == KEY_SIZE*2 + 2 && (argv[i][0] == 'e' || argv[i][0] == 'E') && (argv[i][1] == 'd' || argv[i][1] == 'D')) + argv[i]+=2; + else if (len != KEY_SIZE*2) { + fprintf(stderr, "appbill received an invalid key %s, expected len=%d actual len=%d\n", argv[i], KEY_SIZE*2, len); + return 128; + } + } + return 0; +} + +void key_from_hex(uint8_t* key_in, uint8_t* key_out) { + for (int c = 0; c < 32; ++c) { + uint8_t hn = tolower(key_in[c*2]); + uint8_t ln = tolower(key_in[c*2+1]); + hn = ( hn >= 'a' ? 10 + (hn - 'a') : hn - '0'); + ln = ( ln >= 'a' ? 10 + (ln - 'a') : ln - '0'); + key_out[c] = (hn * 16) + ln; + } +} + +uint64_t uint64_from_bytes(uint8_t* data) { + return + (((uint64_t)(data[0]))<<56) + + (((uint64_t)(data[1]))<<48) + + (((uint64_t)(data[2]))<<40) + + (((uint64_t)(data[3]))<<32) + + (((uint64_t)(data[4]))<<24) + + (((uint64_t)(data[5]))<<16) + + (((uint64_t)(data[6]))<<8) + + (((uint64_t)(data[7]))); +} + + +void uint64_to_bytes(uint8_t* dest, uint64_t x) { + for (int j = 0; j < 8; j++) { + *(dest + (7-j)) = x & 0xff; + x >>= 8; + } +} + + +// returns 1 if found and populates entry_out, 0 if not found +int binary_file_search(FILE* f, uint8_t* key, uint8_t* entry_out, uint64_t* balance_out, size_t* recordno_out, int* error_out) { + + *error_out = 0; + *balance_out = 0; + + // find file size + fseek(f, (size_t)0, SEEK_END); + size_t tablesize = ftell(f); + int recordcount = tablesize / RECORD_SIZE; + + size_t record = recordcount/2; + size_t search_size = (recordcount == 1 ? 1 : record); + + + + uint8_t entry_array[RECORD_SIZE*2]; + uint8_t* entry = entry_array + RECORD_SIZE; + uint8_t* prev_entry = entry_array; + + + while (search_size) { + + if (record == 0) { + // special case, no previous record will be available + fseek(f, 0, SEEK_SET); + int r = fread(entry, 1, RECORD_SIZE, f); + if (r != RECORD_SIZE) { + fprintf(stderr, "failed to read %d bytes\n", RECORD_SIZE); + *error_out = 128; + return 0; + } + memset(prev_entry, 0, RECORD_SIZE); + } else { + fseek(f, (record - 1) * RECORD_SIZE, SEEK_SET); + int r = fread(entry_array, 1, RECORD_SIZE * 2, f); + if (r != RECORD_SIZE*2) { + fprintf(stderr, "failed to read %d bytes\n", RECORD_SIZE*2); + *error_out = 128; + return 0; + } + } + + int search_direction = compar(entry, key); + int check_prev = compar(prev_entry, key); + + if (DEBUG) { + printf("prevrec: %lu\tkey: ", record-1); + print_hex(prev_entry, RECORD_SIZE); + printf("\tsearch dir: %d\n", check_prev); + printf("record: %lu\tkey: ", record); + print_hex(entry, RECORD_SIZE); + printf("\tsearch dir: %d\n", search_direction); + } + + + if (search_direction == 0) { + // get the balance + *balance_out = uint64_from_bytes(entry+32); + if (DEBUG) printf("entry found at record %lu with balance=%lu\n", record, *balance_out); + if (entry_out) + for (int i = 0; i < RECORD_SIZE; ++i) + entry_out[i] = entry[i]; + *recordno_out = record; + return 1; + } + + if (check_prev == 0 && record != 0) { + // get the balance + *balance_out = uint64_from_bytes(prev_entry+32); + if (DEBUG) printf("entry found at record %lu with balance=%lu\n", record-1, *balance_out); + if (entry_out) + for (int i = 0; i < RECORD_SIZE; ++i) + entry_out[i] = prev_entry[i]; + *recordno_out = record-1; + return 1; + } + + if (search_direction != check_prev ) { + // record doesn't exist, it would go between these two records if it did + if (DEBUG) printf("record doesn't exist, would go between\n"); + *recordno_out = record; + return 0; + } + + + search_size /= 2; + + if (search_size < 1) search_size = 1; + + if (search_direction > 0) { + record -= search_size; + } else { + record += search_size; + } + + if (DEBUG) printf("search size: %lu, current record: %lu, check_prev: %d, dir: %d\n", search_size, record, check_prev, search_direction); + + if (record < 0 || record >= recordcount) { + if (DEBUG) + fprintf(stderr, "could not find key record: %lu, recordcount: %d\n", record, recordcount); + if (entry_out) + for (int i = 0; i < RECORD_SIZE; ++i) + entry_out[i] = entry[i]; + *recordno_out = record; + *balance_out = 0; + return 0; + } + + + + *recordno_out = record; + } + + return 0; +} + +// inserts above recordno +// warning: expensive, must copy remaining chunk of file down +int insert_record(FILE* f, uint8_t* entry, size_t recordno) { + + static uint8_t* file_buffer = 0; // we're going to reuse this piece of memory until appbill closes so just alloc once + + if (!file_buffer) + file_buffer = (uint8_t*)malloc(MAX(FILE_BUFFER_SIZE, RECORD_SIZE)); + + if (DEBUG) printf("insert_record called with recno=%lu\n", recordno); + //uint8_t* buffer[FILE_BUFFER_SIZE]; + + size_t offset = recordno * RECORD_SIZE; + + fseek(f, (size_t)0, SEEK_END); + size_t size = ftell(f); + + // inserting 64 bytes at offset + // we need to first compute the short move at the end + + long long tomove = size - offset; + if (tomove <= 0) { + // write the record at the end of the file + if (DEBUG) printf("new write\n"); + return fwrite(entry, RECORD_SIZE, 1, f); + } else { + size_t endpiece = tomove % FILE_BUFFER_SIZE; + + if (DEBUG) printf("endpiece %lu\n", endpiece); + + // the endpiece is always moved, some times there are also no further pieces to move + fseek(f, size - endpiece, SEEK_SET); + fread(file_buffer, 1, endpiece, f); + fseek(f, size - endpiece + RECORD_SIZE, SEEK_SET); + fwrite(file_buffer, 1, endpiece, f); + + size_t cursor = size - endpiece; + + tomove -= endpiece; + + // now if there are any other pieces to move along we can move them + if (size - endpiece >= FILE_BUFFER_SIZE) + for (size_t cursor = size - endpiece - FILE_BUFFER_SIZE; cursor > offset; cursor -= FILE_BUFFER_SIZE) { + if (DEBUG) printf("moving %d sized piece at %lu to %lu - size: %lu - offset: %lu\n", FILE_BUFFER_SIZE, cursor, cursor + RECORD_SIZE, size, offset); + fseek(f, cursor, SEEK_SET); + fread(file_buffer, 1, FILE_BUFFER_SIZE, f); + fseek(f, cursor + RECORD_SIZE, SEEK_SET); + fwrite(file_buffer, 1, FILE_BUFFER_SIZE, f); + } + + // not sure why we need to move this last row down, something is slightly wrong with the math above? + fseek(f, offset, SEEK_SET); + fread(file_buffer, 1, RECORD_SIZE, f); + fseek(f, offset + RECORD_SIZE, SEEK_SET); + fwrite(file_buffer, 1, RECORD_SIZE, f); + + // finally it's safe to emplace our data + fseek(f, offset, SEEK_SET); + return fwrite(entry, 1, RECORD_SIZE, f); + } +} + + +int valid_hex(char* hex, int len) { + char* x = hex; + for (; (x-hex) < len && *x != '\0' && *x != '\n' && *x >= '0' && (*x <= '9' || *x >= 'a' && *x <= 'f' || *x >= 'A' && *x <= 'F'); ++x); + return x-hex == len; +} + +int pass_through_mode(int argc, char** argv) { + // full argc, argv are in tact in this mode + + if (DEBUG) + printf("pass through mode\n"); + + int teepipe[2]; + int error = pipe(teepipe); + if (error) { + fprintf(stderr, "appbill pass through could not create a pipe for teeing fdlist\n"); + return 128; + } + + FILE* teepipeout = fdopen(teepipe[1], "w"); + + // todo: make this all zero copy when someone has time to debug tee and vmsplice readmode + // for now we'll just do a dumb read + + FILE* f = fopen(TABLE_FILE, "rb+"); + if (!f) + f = fopen(TABLE_FILE_2, "rb+"); + + if (!f) { + fprintf(stderr, "could not open %s or %s\n", TABLE_FILE, TABLE_FILE_2); + return 128; + } + + char buf[1024]; + int mode = 0; + int bytes_read = 0; + + int counter = 0; + int toskip = 0; + + uint8_t key[KEY_SIZE]; + + do { + char c = 0; + bytes_read = 0; + while ( (c = getc( stdin )) != EOF && c != ',' && c != '{' && c != '}' && c != '[' && c != ']' && c != '\n' && c != ':' && bytes_read < 1023 ) { + buf[bytes_read++] = c; + putc(c, teepipeout); // make a copy for the next program + } + if (c != EOF) putc(c, teepipeout); // make a copy for the next program + + if (c == EOF) + break; + + if (mode == 2) + continue; + + buf[bytes_read] = '\0'; + + if (mode == 0 && strcmp("\"usrfd\"", buf) == 0) { + mode = 1; + continue; + } else if ( mode == 1 && c == '}' ) { + mode = 2; + continue; + } + + if (buf[0] == '\0' || !mode) + continue; + + ++counter %= 3; + + // this runs if there's an error in the user's public key + if (toskip) { + toskip--; + continue; + } + + + if (DEBUG) + printf("mode=%d counter=%d component `%s`\n", mode, counter%3, buf); + + if (counter == 1) { + // this is the user key + // remove trailing " + if (!buf[strlen(buf)-1] == '"') + continue; + buf[strlen(buf)-1] = '\0'; + + // check the key is valid + if (DEBUG) + printf("key length: %lu, proper length: %d\n", strlen(buf+3), KEY_SIZE*2); + if (DEBUG) + printf("hex: `%s`\n", buf+3); + if (strlen(buf+3) != KEY_SIZE*2 || !valid_hex(buf+3, KEY_SIZE*2)) { + toskip = 2; + if (DEBUG) + printf("invald public key %s\n", buf+3); + continue; + } + + key_from_hex((uint8_t*)buf+3, key); + if (DEBUG) { + printf("parsed key: "); + print_hex(key, KEY_SIZE); + printf("\n"); + } + } else if (counter == 2) { + // this is the user's input fd + + int userfd = 0; + if (!sscanf(buf, "%d", &userfd)) + continue; + + if (DEBUG) printf("mode=2 userfd=%d\n", userfd); + + // there might be some bytes pending on this input, if there are we need to bill for them, one coin per byte + + /*int nbytes = 0; + ioctl(userfd, FIONREAD, &nbytes);*/ + + int64_t to_bill = 0; // and one coin per round for being connected too // no change that to 0 because otherwise malicious nodes can drain accounts! + + //todo: replace all this rubbish with a properly tested zero copy approach + int userpipe[2]; + pipe(userpipe); //todo: handle possible error condition here + FILE* userfile = fdopen(userfd, "r"); + FILE* newuserfile = fdopen(userpipe[1], "w"); + + char x = 0; + while ((x = getc(userfile)) != EOF) { + if (to_bill < (uint64_t)-1) + to_bill++; + putc(x, newuserfile); + } + + fclose(newuserfile); + fclose(userfile); + dup2(userpipe[0], userfd); + + + if (DEBUG) + printf("tobill: %lu\n", to_bill); + + // commence billing + + int error = 0; + uint64_t balance = 0; + size_t recordno = 0; + uint8_t entry[64]; + if (binary_file_search(f, key, entry, &balance, &recordno, &error)) { + // key already exists, update it + if (DEBUG) printf("writing 64 bytes at record:%lu\n", recordno); + fseek(f, recordno*RECORD_SIZE, SEEK_SET); + uint64_t balance = uint64_from_bytes(entry+32); + balance = new_balance(balance, -to_bill); + uint64_to_bytes(entry+32, balance); + fwrite(entry, RECORD_SIZE, 1, f); + } else { + // is user doesn't exist this is an error but we can't do anything about it in passthrough mode so ignore + if (DEBUG) { + printf("user not found key:"); + print_hex(key, KEY_SIZE); + printf("\n"); + } + continue; + } + } + + } while (!feof(stdin)); + + fflush(teepipeout); + + close(teepipe[1]); + dup2(teepipe[0], 0); + + fclose(f); + + execv(argv[1], argv+1); + +} + + + +int credit_mode(int argc, char** argv) { + // argc,v start from first useful arguments + if (argc == 0 || argc % 2 == 1) { + fprintf(stderr, "appbill credit mode requires args like: public_key amount public key amount\n"); + return 128; + } + + if (correct_for_ed_keys(argc, argv, 2, 0)) + return 128; + + // sanity check our inputs + for (int i = 0; i < argc; i += 2) { + for (char* x = argv[i];; ++x) { + if ( x - argv[i] == KEY_SIZE*2 && *x != '\0' ) { + fprintf(stderr, "appbill was supplied an invalid public key\n"); + return 128; + } + + if (*x >= 'a' && *x <= 'f' || *x >= '0' && *x <= '9' || *x >= 'A' && *x <= 'F') + continue; + + if (*x == '\0') + break; + + fprintf(stderr, "appbill was supplied an invalid public key (not hex) char=%c\n", *x); + return 128; + } + + for (char* x = argv[i+1]; *x; ++x) + if ( ! (*x >= '0' && *x <= '9' || *x == '-') ) { + fprintf(stderr, "appbill was supplied invalid amount to credit, must be decimal integer entry=%s\n", argv[i+1]); + return 128; + } + + int64_t to_credit = 0; + if (!sscanf(argv[i+1], "%ld", &to_credit)) { + fprintf(stderr, "appbill was supplied invalid amount to credit, must be decimal integer entry=%s\n", argv[i+1]); + return 128; + } + } + + FILE* f = fopen(TABLE_FILE, "rb+"); + if (!f) + f = fopen(TABLE_FILE_2, "rb+"); + + if (!f) { + fprintf(stderr, "could not open %s or %s\n", TABLE_FILE, TABLE_FILE_2); + return 128; + } + + // now the expensive bit + for (int i = 0; i < argc; i += 2) { + uint8_t key[32]; + key_from_hex((uint8_t*)argv[i], key); + + int64_t to_credit = 0; + if (!sscanf(argv[i+1], "%ld", &to_credit)) // this has been sanity checked above + continue; + + int error = 0; + uint64_t balance = 0; + size_t recordno = 0; + uint8_t entry[64]; + if (binary_file_search(f, key, entry, &balance, &recordno, &error)) { + // key already exists, update it + if (DEBUG) printf("writing 64 bytes at record:%lu\n", recordno); + fseek(f, recordno*RECORD_SIZE, SEEK_SET); + uint64_t balance = uint64_from_bytes(entry+32); + balance = new_balance(balance, to_credit); + uint64_to_bytes(entry+32, balance); + fwrite(entry, RECORD_SIZE, 1, f); + + } else { + + if (DEBUG) printf("key needs to be inserted\n"); + // key doesn't exist, insert it + uint8_t new_entry[RECORD_SIZE]; + for (int i = 0; i < KEY_SIZE; ++i) + new_entry[i] = key[i]; + uint64_t balance = new_balance(0, to_credit); + + uint64_to_bytes(new_entry+32, balance); + + for (int i = KEY_SIZE + 8; i < RECORD_SIZE; ++i) + new_entry[i] = 0; + + // get the existing entry + fseek(f, recordno * RECORD_SIZE, SEEK_SET); + fread(entry, 1, RECORD_SIZE, f); + + int insert_direction = compar(entry, new_entry); + + insert_record(f, new_entry, recordno + (insert_direction < 0 ? 1 : 0)); + } + + } + + + fclose(f); + return 0; +} + +int check_mode(int argc, char** argv, int print_balances) { + + if (DEBUG) + printf("check mode\n"); + + if (argc > 14 && !print_balances) { + fprintf(stderr, "appbill can only take up to 7 keys at a time\n"); + return 128; + } + + if (correct_for_ed_keys(argc, argv, 2, 0)) + return 128; + + for (int i = 0; i < argc; i+=2) { + // check the pubkey + for (char* x = argv[i];; ++x) { + if ( x - argv[i] == KEY_SIZE*2 && *x != '\0' ) { + fprintf(stderr, "appbill was supplied an invalid public key\n"); + return 128; + } + + if (*x >= 'a' && *x <= 'f' || *x >= '0' && *x <= '9' || *x >= 'A' && *x <= 'F') + continue; + + if (*x == '\0') + break; + + fprintf(stderr, "appbill was supplied an invalid public key (not hex) char=%c\n", *x); + return 128; + } + + // check the bytecount + for (char* x = argv[i+1]; *x != '\0'; ++x) { + if (*x >= '0' && *x <= '9') + continue; + fprintf(stderr, "appbill was supplied invalid byte count %s\n", argv[i+1]); + return 128; + } + + } + + if (print_balances) + printf("{\n"); + + // open app bill table + + FILE* f = fopen(TABLE_FILE, "rb"); + if (!f) + f = fopen(TABLE_FILE_2, "rb"); + + if (!f) { + fprintf(stderr, "could not open table file at %s or %s\n", TABLE_FILE, TABLE_FILE_2); + return 128; + } + + int bits[7]; + for (int i = 0; i < 7; ++i) + bits[i] = 0; + + // loop keys, check balances + for (int i = 0, j = 0; i < argc; i+=2, ++j) { + // convert the argv from hex to binary + uint8_t key[32]; + key_from_hex((uint8_t*)argv[i], key); + + uint32_t bytecount = 0; + sscanf(argv[i+1], "%d", &bytecount); + + int error = 0; + uint64_t balance = 0; + size_t recordno = 0; + if (binary_file_search(f, key, 0, &balance, &recordno, &error)) { + if (j < 7) bits[j] = balance > bytecount; + if (print_balances) { + printf("\t\""); + print_hex(key, KEY_SIZE); + printf("\": %lu%s", balance, (i == argc-1 ? "\n": ",\n")); + } + } + } + + if (print_balances) + printf("}"); + fclose(f); + + if (DEBUG) + for (int i = 0; i < 7; ++i) + printf("bit %d: %d\n", i, bits[i]); + + return bits[0] * 64 + bits[1] * 32 + bits[2] * 16 + bits[3] * 8 + bits[4] * 4 + bits[5] * 2 + bits[6]; +} + +int main(int argc, char** argv) { + + + + // input checks + + int mode = 0; // mode 0 is passthrough [ writes ] + + if (argc >= 2 && strcmp(argv[1], "--credit") == 0) + mode = 1; // mode 1 credit mode [ writes ] + + if (argc >= 2 && strcmp(argv[1], "--check") == 0) + mode = 2; // mode 2 check mode [ read only ] + + if (argc >= 2 && strcmp(argv[1], "--balance") == 0) + mode = 3; // mode 3 balance mode [ read only ] + + + if (mode == 0) { + if (argc < 2) { + fprintf(stderr, "appbill requires an executable to pass execution to as an argument when running in pass through mode\n"); + return 128; + } + return pass_through_mode(argc, argv); + } + + if (argc < 3) { + fprintf(stderr, "appbill was not supplied sufficient arguments\n"); + return 128; + } + + argc-=2; + argv+=2; + + if (mode == 1) + return credit_mode(argc, argv); + + if (mode == 2 || mode == 3) + return check_mode(argc, argv, mode == 3); + + fprintf(stderr, "unknown mode, execution should not reach here\n"); + + return 128; +} diff --git a/src/conf.cpp b/src/conf.cpp index 6e009686..843bfd48 100644 --- a/src/conf.cpp +++ b/src/conf.cpp @@ -218,11 +218,21 @@ int load_config() cfg.binary = d["binary"].GetString(); cfg.binargs = d["binargs"].GetString(); + cfg.appbill = d["appbill"].GetString(); + cfg.appbillargs = d["appbillargs"].GetString(); + // Populate runtime contract execution args. if (!cfg.binargs.empty()) boost::split(cfg.runtime_binexec_args, cfg.binargs, boost::is_any_of(" ")); - cfg.runtime_binexec_args.insert(cfg.runtime_binexec_args.begin(), cfg.binary); + cfg.runtime_binexec_args.insert(cfg.runtime_binexec_args.begin(), ( cfg.binary[0] == '/' ? cfg.binary : util::realpath( ctx.contractdir + "/bin/" + cfg.binary ) ) ); + + + // Populate runtime app bill args. + if (!cfg.appbillargs.empty()) + boost::split(cfg.runtime_appbill_args, cfg.appbillargs, boost::is_any_of(" ")); + + cfg.runtime_appbill_args.insert(cfg.runtime_appbill_args.begin(), ( cfg.appbill[0] == '/' ? cfg.appbill : util::realpath( ctx.contractdir + "/bin/" + cfg.appbill ) ) ); // Uncomment for docker-based execution. // std::string volumearg; @@ -311,6 +321,9 @@ int save_config() d.AddMember("seckeyhex", rapidjson::StringRef(cfg.seckeyhex.data()), allocator); d.AddMember("binary", rapidjson::StringRef(cfg.binary.data()), allocator); d.AddMember("binargs", rapidjson::StringRef(cfg.binargs.data()), allocator); + d.AddMember("appbill", rapidjson::StringRef(cfg.appbill.data()), allocator); + d.AddMember("appbillargs", rapidjson::StringRef(cfg.appbillargs.data()), allocator); + d.AddMember("listenip", rapidjson::StringRef(cfg.listenip.data()), allocator); d.AddMember("listenip", rapidjson::StringRef(cfg.listenip.data()), allocator); rapidjson::Value peers(rapidjson::kArrayType); @@ -540,7 +553,7 @@ int is_schema_valid(const rapidjson::Document &d) const char *cfg_schema = "{" "\"type\": \"object\"," - "\"required\": [ \"mode\", \"version\", \"pubkeyhex\", \"seckeyhex\", \"binary\", \"binargs\", \"listenip\"" + "\"required\": [ \"mode\", \"version\", \"pubkeyhex\", \"seckeyhex\", \"binary\", \"binargs\", \"appbill\", \"appbillargs\", \"listenip\"" ", \"peers\", \"unl\", \"pubport\", \"peerport\", \"roundtime\"" ", \"pubmaxsize\", \"pubmaxcpm\", \"pubmaxbadmpm\", \"pubmaxcons\"" ", \"peermaxsize\", \"peermaxcpm\", \"peermaxdupmpm\", \"peermaxbadmpm\", \"peermaxbadsigpm\", \"peermaxcons\"" @@ -552,6 +565,8 @@ int is_schema_valid(const rapidjson::Document &d) "\"seckeyhex\": { \"type\": \"string\" }," "\"binary\": { \"type\": \"string\" }," "\"binargs\": { \"type\": \"string\" }," + "\"appbill\": { \"type\": \"string\" }," + "\"appbillargs\": { \"type\": \"string\" }," "\"listenip\": { \"type\": \"string\" }," "\"peers\": {" "\"type\": \"array\"," diff --git a/src/conf.hpp b/src/conf.hpp index 5a924f4d..7840452e 100644 --- a/src/conf.hpp +++ b/src/conf.hpp @@ -46,6 +46,8 @@ struct contract_config std::string pubkey; // Contract public key bytes std::string seckey; // Contract secret key bytes std::vector runtime_binexec_args; // Contract binary execution args used during runtime. + std::vector runtime_appbill_args; // Appbill execution args used during runtime. + // Config elements which are loaded from the config file. @@ -55,6 +57,8 @@ struct contract_config std::string keytype; // Key generation algorithm used by libsodium std::string binary; // Full path to the contract binary std::string binargs; // CLI arguments to pass to the contract binary + std::string appbill; // binary to execute for appbill + std::string appbillargs; // any arguments to supply to appbill binary by default std::string listenip; // The IPs to listen on for incoming connections std::unordered_map peers; // Map of peers keyed by ":" concatenated format std::unordered_set unl; // Unique node list (list of binary public keys) @@ -114,4 +118,4 @@ void change_operating_mode(const OPERATING_MODE mode); } // namespace conf -#endif \ No newline at end of file +#endif diff --git a/src/cons/cons.cpp b/src/cons/cons.cpp index 16162cde..f430ee6b 100644 --- a/src/cons/cons.cpp +++ b/src/cons/cons.cpp @@ -14,6 +14,7 @@ #include "ledger_handler.hpp" #include "state_handler.hpp" #include "cons.hpp" +#include "../statefs/state_common.hpp" #include "../statefs/state_store.hpp" namespace p2pmsg = fbschema::p2pmsg; @@ -320,9 +321,12 @@ void verify_and_populate_candidate_user_inputs() { for (const auto &[pubkey, umsgs] : p.user_messages) { - // Populate user list. + // Populate user list with this user's pubkey. ctx.candidate_users.emplace(pubkey); + // Collect valid inputs for this user. + std::unordered_map valid_inputs; + for (const usr::user_submitted_message &umsg : umsgs) { const std::string sig_hash = crypto::get_hash(umsg.sig); @@ -333,8 +337,6 @@ void verify_and_populate_candidate_user_inputs() // Verify the signature of the message content. if (crypto::verify(umsg.content, umsg.sig, pubkey) == 0) { - // TODO: Also verify XRP payment token/AppBill requirements. - std::string nonce; std::string input; uint64_t maxledgerseqno; @@ -348,7 +350,7 @@ void verify_and_populate_candidate_user_inputs() // Append the hash of the message signature to get the final hash. hash.append(sig_hash); - ctx.candidate_user_inputs.try_emplace( + valid_inputs.try_emplace( hash, candidate_user_input(pubkey, std::move(input), maxledgerseqno)); } @@ -359,11 +361,81 @@ void verify_and_populate_candidate_user_inputs() LOG_DBG << "Duplicate user message."; } } + + // Verify the accumulated input length with app bill before adding to candidate inputs. + size_t total_input_len = 0; + for (const auto &[hash, cand_input] : valid_inputs) + total_input_len += cand_input.input.size(); + + if (verify_appbill_check(pubkey, total_input_len)) + ctx.candidate_user_inputs.merge(valid_inputs); + + // TODO: report back to the user if the inputs didn't make it into consensus due to some reason. } } p2p::ctx.collected_msgs.nonunl_proposals.clear(); } +/** + * Executes the appbill and verifies whether the user has enough account balance to process the provided input. + * @param pubkey User binary pubkey. + * @param input_len Total bytes length of user input. + * @return Whether the user is allowed to process the input or not. + */ +bool verify_appbill_check(std::string_view pubkey, const size_t input_len) +{ + // If appbill not enabled always green light the input. + if (conf::cfg.appbill.empty()) + return true; + + // execute appbill in --check mode to verify this user can submit a packet/connection to the network + // todo: this can be made more efficient, appbill --check can process 7 at a time + + // Fill appbill args + const int len = conf::cfg.runtime_appbill_args.size() + 4; + char *execv_args[len]; + for (int i = 0; i < conf::cfg.runtime_appbill_args.size(); i++) + execv_args[i] = conf::cfg.runtime_appbill_args[i].data(); + char option[] = "--check"; + execv_args[len - 4] = option; + // add the hex encoded public key as the last parameter + std::string hexpubkey; + util::bin2hex(hexpubkey, reinterpret_cast(pubkey.data()), pubkey.size()); + std::string inputsize = std::to_string(input_len); + execv_args[len - 3] = hexpubkey.data(); + execv_args[len - 2] = inputsize.data(); + execv_args[len - 1] = NULL; + + int pid = fork(); + if (pid == 0) + { + // before execution chdir into a valid the latest state data directory that contains an appbill.table + chdir(statefs::current_ctx.datadir.c_str()); + int ret = execv(execv_args[0], execv_args); + LOG_ERR << "Appbill process execv failed: " << ret; + return false; + } + else + { + // app bill in check mode takes a very short period of time to execute, typically 1ms + // so we will blocking wait for it here + int status = 0; + waitpid(pid, &status, 0); //todo: check error conditions here + status = WEXITSTATUS(status); + if (status != 128 && status != 0) + { + // this user's key passed appbill + return true; + } + else + { + // user's key did not pass, do not add to user input candidates + LOG_DBG << "Appbill validation failed " << hexpubkey << " return code was " << status; + return false; + } + } +} + p2p::proposal create_stage0_proposal() { // The proposal we are going to emit in stage 0. diff --git a/src/cons/cons.hpp b/src/cons/cons.hpp index 7b99173f..1c2f9d17 100644 --- a/src/cons/cons.hpp +++ b/src/cons/cons.hpp @@ -118,6 +118,8 @@ void broadcast_nonunl_proposal(); void verify_and_populate_candidate_user_inputs(); +bool verify_appbill_check(std::string_view pubkey, const size_t input_len); + p2p::proposal create_stage0_proposal(); p2p::proposal create_stage123_proposal(vote_counter &votes); diff --git a/src/proc/proc.cpp b/src/proc/proc.cpp index 835bc94a..e66b2737 100644 --- a/src/proc/proc.cpp +++ b/src/proc/proc.cpp @@ -101,11 +101,21 @@ int exec_contract(const contract_exec_args &args) LOG_DBG << "Starting contract process..."; + const bool using_appbill = !conf::cfg.appbill.empty(); + int len = conf::cfg.runtime_binexec_args.size() + 1; + if (using_appbill) + len += conf::cfg.runtime_appbill_args.size(); + // Fill process args. - char *execv_args[conf::cfg.runtime_binexec_args.size() + 1]; - for (int i = 0; i < conf::cfg.runtime_binexec_args.size(); i++) - execv_args[i] = conf::cfg.runtime_binexec_args[i].data(); - execv_args[conf::cfg.runtime_binexec_args.size()] = NULL; + char *execv_args[len]; + int j = 0; + if (using_appbill) + for (int i = 0; i < conf::cfg.runtime_appbill_args.size(); i++, j++) + execv_args[i] = conf::cfg.runtime_appbill_args[i].data(); + + for (int i = 0; i < conf::cfg.runtime_binexec_args.size(); i++, j++) + execv_args[j] = conf::cfg.runtime_binexec_args[i].data(); + execv_args[len - 1] = NULL; int ret = execv(execv_args[0], execv_args); LOG_ERR << "Contract process execv failed: " << ret; @@ -718,4 +728,4 @@ void deinit() kill(statemon_pid, SIGINT); } -} // namespace proc \ No newline at end of file +} // namespace proc diff --git a/src/util.cpp b/src/util.cpp index 918cecb9..8c3f34f0 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -197,4 +197,4 @@ std::string realpath(std::string path) return buffer.data(); } -} // namespace util \ No newline at end of file +} // namespace util diff --git a/src/util.hpp b/src/util.hpp index 37b342e9..ca03c255 100644 --- a/src/util.hpp +++ b/src/util.hpp @@ -80,4 +80,4 @@ std::string realpath(std::string path); } // namespace util -#endif \ No newline at end of file +#endif diff --git a/test/vm-cluster/cluster.sh b/test/vm-cluster/cluster.sh index 02b4d1e6..83a4016b 100755 --- a/test/vm-cluster/cluster.sh +++ b/test/vm-cluster/cluster.sh @@ -7,10 +7,11 @@ mode=$1 hpcore=$(realpath ../..) -if [ "$mode" = "new" ] || [ "$mode" = "run" ] || [ "$mode" = "update" ] || [ "$mode" = "kill" ]; then +if [ "$mode" = "new" ] || [ "$mode" = "update" ] || [ "$mode" = "run" ] || [ "$mode" = "check" ] || \ + [ "$mode" = "connect" ] || [ "$mode" = "kill" ] || [ "$mode" = "reboot" ] || [ "$mode" = "ssh" ]; then echo "" else - echo "Invalid command. new | run | update | kill expected." + echo "Invalid command. [ new | update | run | check | connect | kill | reboot | ssh ] expected." exit 1 fi @@ -22,11 +23,39 @@ if [ $mode = "run" ]; then exit 0 fi +if [ $mode = "check" ]; then + let nodeid=$2-1 + vmip=${vmips[$nodeid]} + sshpass -p $vmpass ssh geveo@$vmip 'echo hpcore pid:$(pidof hpcore) hpstatemon pid:$(pidof hpstatemon)' + exit 0 +fi + +if [ $mode = "connect" ]; then + let nodeid=$2-1 + vmip=${vmips[$nodeid]} + sshpass -p $vmpass ssh geveo@$vmip 'tail -f nohup.out' + exit 0 +fi + if [ $mode = "kill" ]; then let nodeid=$2-1 vmip=${vmips[$nodeid]} - sshpass -p $vmpass ssh geveo@$vmip 'sudo kill $(pidof hpcore)' - sshpass -p $vmpass ssh geveo@$vmip 'sudo kill $(pidof hpstatemon)' + sshpass -p $vmpass ssh geveo@$vmip 'sudo kill $(pidof hpcore) > /dev/null 2>&1' + sshpass -p $vmpass ssh geveo@$vmip 'sudo kill $(pidof hpstatemon) > /dev/null 2>&1' + exit 0 +fi + +if [ $mode = "reboot" ]; then + let nodeid=$2-1 + vmip=${vmips[$nodeid]} + sshpass -p $vmpass ssh geveo@$vmip 'sudo reboot' + exit 0 +fi + +if [ $mode = "ssh" ]; then + let nodeid=$2-1 + vmip=${vmips[$nodeid]} + sshpass -p $vmpass ssh geveo@$vmip $3 exit 0 fi