Skip to content

Instantly share code, notes, and snippets.

@vbar
Last active June 24, 2022 10:58
Show Gist options
  • Save vbar/d724c1ec91488c197f113c62f324a262 to your computer and use it in GitHub Desktop.
Save vbar/d724c1ec91488c197f113c62f324a262 to your computer and use it in GitHub Desktop.

Firewall is an example of cooperating hooks: firewall hook instances filter incoming payments, based on shared state maintained by blacklist hook.

To cooperate, the hooks need information about each other, which must be obtained before deploying them, because it's passed to them as install-time parameters.

to test:

  • make sure Hooks Builder has at least 4 accounts: Alice, Bob, Carol and Carlos
  • run key.js to get a public key of the blacklist admin account, e.g. Alice; it asks for that account's secret key
    • verify the script is using it just to derive the public key
    • if you don't believe the script, or its dependencies, do the conversion in a trusted environment
  • compile blacklist.c and deploy it to Bob account
    • for ttPAYMENT + ttACCOUNT_SET
    • noting the hook namespace and saving it somewhere
      • with "admin" param set to Alice public key obtained above
  • run block-unblock.js to block an account; the script requires 4 parameters:
    1. admin is the account authorized to sign blacklist updates, i.e. Alice; the script needs its private key
    2. blacklist is the account sending AccountSet, i.e. Bob; its private key is used to sign that transaction
    3. suspect is the 3rd-party account to be blocked or unblocked, i.e. Carlos; its private key is not needed
    4. execution mode should be set to 1, to block
  • run decode.js to convert Bob account to binary form (the hook could do that, but it's extra computation that doesn't need to be replicated on chain) and save it somewhere
    • enter Bob account as the script parameter
  • compile firewall.c and deploy it to Carol account, with 2 parameters:
    1. "blaccid" parameter set to Bob binary account
    2. and "blns" parameter set to the blacklist hook namespace
  • set up payment transaction from Carlos to Carol
  • open debug stream filtered on Carol
  • run the transaction, see it rejected:
    • "Firewall: Blocking transaction from blacklisted account." in the debug stream
    • "[tecHOOK_REJECTED] Rejected by hook on sending or receiving account." in the development log
  • run block-unblock.js to unblock Carlos account; the parameters are mostly as before:
    1. admin is the account authorized to sign blacklist updates, i.e. Alice; the script needs its private key
    2. blacklist is the account sending AccountSet, i.e. Bob; its private key is used to sign that transaction
    3. suspect is the 3rd-party account to be blocked or unblocked, i.e. Carlos; its private key is not needed
    4. execution mode should be set to 0, to unblock
  • repeat the payment transaction, see it succeed:
    • "Firewall: Allowing transaction." in the debug stream
    • "[tesSUCCESS] The transaction was applied. Only final in a validated ledger." in the development log
#include "hookapi.h"
int64_t hook(uint32_t reserved)
{
// check for the presence of a memo
uint8_t memos[2048];
int64_t memos_len = otxn_field(SBUF(memos), sfMemos);
uint32_t payload_len = 0, signature_len = 0, publickey_len = 0;
uint8_t* payload_ptr = 0, *signature_ptr = 0, *publickey_ptr = 0;
if (memos_len <= 0)
accept(SBUF("Blacklist: Passing non-memo incoming transaction."), 0);
/**
* 'Signed Memos' for hooks are supplied in triples in the following 'default' format:
* NB: The +1 identifies the payload, you may provide multiple payloads
* Memo: { MemoData: <app data>, MemoFormat: "signed/payload+1", MemoType: [application defined] }
* Memo: { MemoData: <signature>, MemoFormat: "signed/signature+1", MemoType: [application defined] }
* Memo: { MemoData: <public_key>, MemoFormat: "signed/publickey+1", MemoType: [application defined] }
**/
// loop through the three memos (if 3 are even present) to parse out the relevant fields
for (int i = 0; GUARD(3), i < 3; ++i)
{
// the memos are presented in an array object, which we must index into
int64_t memo_lookup = sto_subarray(memos, memos_len, i);
TRACEVAR(memo_lookup);
if (memo_lookup < 0)
rollback(SBUF("Blacklist: Memo transaction did not contain correct format."), 30);
// if the subfield/array lookup is successful we must extract the two pieces of returned data
// which are, respectively, the offset at which the field occurs and the field's length
uint8_t* memo_ptr = SUB_OFFSET(memo_lookup) + memos;
uint32_t memo_len = SUB_LENGTH(memo_lookup);
trace(SBUF("Memo: "), memo_ptr, memo_len, 1);
// memos are nested inside an actual memo object, so we need to subfield
// equivalently in JSON this would look like memo_array[i]["Memo"]
memo_lookup = sto_subfield(memo_ptr, memo_len, sfMemo);
memo_ptr = SUB_OFFSET(memo_lookup) + memo_ptr;
memo_len = SUB_LENGTH(memo_lookup);
// now we lookup the subfields of the memo itself
// again, equivalently this would look like memo_array[i]["Memo"]["MemoData"], ... etc.
int64_t data_lookup = sto_subfield(memo_ptr, memo_len, sfMemoData);
int64_t format_lookup = sto_subfield(memo_ptr, memo_len, sfMemoFormat);
TRACEVAR(data_lookup);
TRACEVAR(format_lookup);
// if any of these lookups fail the request is malformed
if (data_lookup < 0 || format_lookup < 0)
rollback(SBUF("Blacklist: Memo transaction did not contain correct memo format."), 40);
// care must be taken to add the correct pointer to an offset returned by sub_array or sub_field
// since we are working relative to the specific memo we must add memo_ptr, NOT memos or something else
uint8_t* data_ptr = SUB_OFFSET(data_lookup) + memo_ptr;
uint32_t data_len = SUB_LENGTH(data_lookup);
uint8_t* format_ptr = SUB_OFFSET(format_lookup) + memo_ptr;
uint32_t format_len = SUB_LENGTH(format_lookup);
// we can use a helper macro to compare the format fields and determine which MemoData is assigned
// to each pointer. Note that the last parameter here tells the macro how many times we will hit this
// line so it in turn can correctly configure its GUARD(), otherwise we will get a guard violation
int is_payload = 0, is_signature = 0, is_publickey = 0;
BUFFER_EQUAL_STR_GUARD(is_payload, format_ptr, format_len, "signed/payload+1", 3);
BUFFER_EQUAL_STR_GUARD(is_signature, format_ptr, format_len, "signed/signature+1", 3);
BUFFER_EQUAL_STR_GUARD(is_publickey, format_ptr, format_len, "signed/publickey+1", 3);
// assign the pointers according to the detected MemoFormat
if (is_payload)
{
payload_ptr = data_ptr;
payload_len = data_len;
} else if (is_signature)
{
signature_ptr = data_ptr;
signature_len = data_len;
} else if (is_publickey)
{
publickey_ptr = data_ptr;
publickey_len = data_len;
}
}
if (!(payload_ptr && signature_ptr && publickey_ptr))
rollback(SBUF("Blacklist: Memo transaction did not contain XLS14 format."), 50);
// check the public key is correct
if (publickey_len != 33)
rollback(SBUF("Blacklist: Memo public key wrong length."), 55);
uint8_t blacklist_key[33];
int64_t prv = hook_param(SBUF(blacklist_key), (uint32_t)"admin", 5);
if (prv != sizeof(blacklist_key))
{
TRACEVAR(prv);
rollback(SBUF("Blacklist: \"admin\" parameter missing"), 56);
}
TRACEHEX(blacklist_key);
int equal = 0;
BUFFER_EQUAL(equal, blacklist_key, publickey_ptr, 33);
if (!equal)
rollback(SBUF("Blacklist: Invalid admin public key."), 57);
// check the signature is valid
if (!util_verify(payload_ptr, payload_len,
signature_ptr, signature_len,
blacklist_key, 33))
rollback(SBUF("Blacklist: Invalid signature in memo."), 60);
// execution to here means that BUFFER<payload_ptr,payload_len> contains a validly signed object
// now check if it is properly constructed
// the expected format is a generic STObject containing
// - at least: sfFlags sfSequence sfTemplate(ARRAY){sfAccount}
// Flags 0 means add and Flags 1 means remove
// Sequence must be greater than the previously used Sequence (timestamp is desirable but not mandated)
// Sequence prevents replay attacks
// ARRAY must contain at least one sfAccount
int64_t lookup_flags = sto_subfield(payload_ptr, payload_len, sfFlags);
int64_t lookup_seq = sto_subfield(payload_ptr, payload_len, sfSequence);
int64_t lookup_array = sto_subfield(payload_ptr, payload_len, sfTemplate);
TRACEVAR(lookup_flags);
TRACEVAR(lookup_seq);
TRACEVAR(lookup_array);
if (lookup_seq < 0 || lookup_flags < 0 || lookup_array < 0)
rollback(SBUF("Blacklist: Validly signed memo lacked required STObject fields."), 70);
// extract the actual transaction details, again taking care to add the correct pointer to the offset
uint32_t seq = UINT32_FROM_BUF(SUB_OFFSET(lookup_seq) + payload_ptr);
uint32_t flags = UINT32_FROM_BUF(SUB_OFFSET(lookup_flags) + payload_ptr);
uint8_t* array_ptr = SUB_OFFSET(lookup_array) + payload_ptr;
int array_len = SUB_LENGTH(lookup_array);
// get the previous sequence number from the hook state (this is the 0 key)
uint8_t state_request[32];
uint8_t seq_buffer[4];
CLEARBUF(state_request);
if (state(SBUF(seq_buffer), SBUF(state_request)) != 4)
{
// first run
} else
{
if (seq <= UINT32_FROM_BUF(seq_buffer))
rollback(SBUF("Blacklist: Sequence number was less than previous sequence."), 75);
}
// update sequence number
UINT32_TO_BUF(seq_buffer, seq);
if (state_set(SBUF(seq_buffer), SBUF(state_request)) != 4)
rollback(SBUF("Blacklist: Sequence number could not be updated."), 77);
// we will accept at most 5 accounts in the array
// increasing the limit is a good way to see what happens when a hook gets too big to deploy
int processed_count = 0;
for (int i = 0; GUARD(5), i < 5; ++i)
{
int64_t lookup_array_entry = sto_subarray(array_ptr, array_len, i);
TRACEVAR(lookup_array_entry);
if (lookup_array_entry < 0)
break; // ran out of array entries to process
uint8_t* array_entry_ptr = SUB_OFFSET(lookup_array_entry) + array_ptr;
uint32_t array_entry_len = SUB_LENGTH(lookup_array_entry);
// this will return the actual payload inside the sfAccount inside the array entry
int64_t lookup_acc = sto_subfield(array_entry_ptr, array_entry_len, sfAccount);
if (lookup_acc < 0)
rollback(SBUF("Blacklist: Invalid array entry, expecting sfAccount."), 80);
uint8_t* acc_ptr = SUB_OFFSET(lookup_acc) + array_entry_ptr;
uint32_t acc_len = SUB_LENGTH(lookup_acc);
if (acc_len != 20)
rollback(SBUF("Blacklist: Invalid sfAccount, expecting length = 20."), 90);
uint8_t buffer[1] = {1}; // nominally we will simply a store a single byte = 1 for a blacklisted account
uint32_t len = flags == 1 ? 1 : 0; // we will pass length = 0 to state_set for a delete operation
if (state_set(buffer, len, acc_ptr, acc_len) == len)
processed_count++;
else
trace(SBUF("Blacklist: Failed to update state for the following account."), acc_ptr, acc_len, 1);
}
RBUF(result_buffer, result_len, "Blacklist: Processed + ", processed_count);
if (flags == 0)
result_buffer[21] = '-';
accept(result_buffer, result_len, 0);
return 0;
}
import lib from "https://esm.sh/xrpl-accountlib?bundle";
import bin from "https://esm.sh/ripple-binary-codec?bundle";
import { XrplClient } from "https://esm.sh/xrpl-client?bundle";
import keypairs from "https://esm.sh/ripple-keypairs?bundle";
// Alice
const admin_secret = "{{ customize_input admin_secret type='select' attach='account_secret' }}";
const admin_keypair = lib.derive.familySeed(admin_secret);
// Bob
const blacklist_secret = "{{ customize_input blacklist_secret type='select' attach='account_secret' }}";
const blacklist_keypair = lib.derive.familySeed(blacklist_secret);
const blacklist_account = keypairs.deriveAddress(blacklist_keypair.keypair.publicKey);
// Carlos
const suspect_account = "{{ customize_input suspect_account type='select' attach='account_address' }}";
const exec_mode = "{{ customize_input exec_mode title='Block=1, Unblock=0' }}";
if ((exec_mode !== "1") && (exec_mode !== "0"))
{
console.error("unknown execution mode \"" + exec_mode + "\" - must be either 0 or 1");
throw "invalid argument";
}
const flag = (exec_mode === "1") ? 1 : 0;
const client = new XrplClient('wss://hooks-testnet-v2.xrpl-labs.com');
const main = async () => {
let blacklist_instruction = bin.encode(
{
Sequence: Math.floor(Date.now()/1000),
Flags: flag,
Template: [
{
Account: suspect_account
}
]
});
const { account_data } = await client.send({ command: 'account_info', 'account': blacklist_account });
if (!account_data) {
console.log('Blacklist account not found.');
client.close();
return;
}
console.log("sequence", account_data.Sequence);
const tx = {
Account: blacklist_account,
TransactionType: 'AccountSet',
Fee: '1200000',
Memos: [
{
Memo: {
MemoData: blacklist_instruction,
MemoFormat: "signed/payload+1",
MemoType: "liteacc/payment"
}
},
{
Memo:{
MemoData: keypairs.sign(blacklist_instruction, admin_keypair.keypair.privateKey),
MemoFormat: "signed/signature+1",
MemoType: "liteacc/signature"
}
},
{
Memo:{
MemoData: admin_keypair.keypair.publicKey,
MemoFormat: "signed/publickey+1",
MemoType: "liteacc/publickey"
}
}
],
Sequence: account_data.Sequence
};
hexlify_memos(tx);
// console.log(JSON.stringify(tx.Memos));
const {signedTransaction} = lib.sign(tx, blacklist_keypair);
const submit = await client.send({ command: 'submit', 'tx_blob': signedTransaction });
console.log(submit);
console.log('Shutting down...');
client.close();
};
function hexlify_memos(x)
{
if (!("Memos" in x))
return;
for (let y = 0; y < x["Memos"].length; ++y)
{
let Memo = x["Memos"][y]["Memo"];
let Fields = ["MemoFormat", "MemoType", "MemoData"];
for (let z = 0; z < Fields.length; ++z)
{
if (Fields[z] in Memo)
{
let u = Memo[Fields[z]].toUpperCase()
if (u.match(/^[0-9A-F]+$/))
{
Memo[Fields[z]] = u;
continue;
}
let v = Memo[Fields[z]], q = "";
for (let i = 0; i < v.length; ++i)
{
q += Number(v.charCodeAt(i)).toString(16).padStart(2, '0');
}
Memo[Fields[z]] = q.toUpperCase();
}
}
}
}
main()
import rac from "https://esm.sh/ripple-address-codec?bundle";
const a = "{{ customize_input account type='select' attach='account_address' }}",
b = rac.decodeAccountID(a),
h = b.toString('hex').toUpperCase();
console.log(h);
#include "hookapi.h"
int64_t hook(uint32_t reserved)
{
GUARD(1);
// fetch the originating account ID
uint8_t otxn_accid[20];
if (otxn_field(otxn_accid, 20, sfAccount) != 20)
rollback(SBUF("Firewall: Could not fetch sfAccount from originating transaction!!!"), 1);
uint8_t blacklist_ns[32];
int64_t prv = hook_param(SBUF(blacklist_ns), (uint32_t)"blns", 4);
if (prv != sizeof(blacklist_ns))
{
TRACEVAR(prv);
rollback(SBUF("Firewall: \"blns\" parameter missing"), 5);
}
uint8_t blacklist_accid[32];
prv = hook_param(SBUF(blacklist_accid), (uint32_t)"blaccid", 7);
if (prv < 20)
{
TRACEVAR(prv);
rollback(SBUF("Firewall: \"blaccid\" parameter missing"), 10);
}
// look up the account ID in the foreign state (blacklist account's hook state)
uint8_t blacklist_status[1] = { 0 };
int64_t lookup = state_foreign(SBUF(blacklist_status), SBUF(otxn_accid), SBUF(blacklist_ns), blacklist_accid, 20);
if (lookup == INVALID_ACCOUNT)
trace(SBUF("Firewall: Warning specified blacklist account does not exist."), 0, 0, 0);
if (blacklist_status[0] == 0)
accept(SBUF("Firewall: Allowing transaction."), 0);
rollback(SBUF("Firewall: Blocking transaction from blacklisted account."), 1);
return 0;
}
import lib from "https://esm.sh/xrpl-accountlib?bundle";
// Alice
const admin_secret = "{{ customize_input account type='select' attach='account_secret' title='Admin Secret' }}";
const admin_keypair = lib.derive.familySeed(admin_secret);
console.log(admin_keypair.keypair.publicKey);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment