Demo M-N multisig library in Lua
This commit is contained in:
42
programs/native/solua/multisig.lua
Normal file
42
programs/native/solua/multisig.lua
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
-- M-N Multisig. Pass in a table "{m=M, n=N, tokens=T}" where M is the number
|
||||||
|
-- of signatures required, and N is a list of the pubkeys identifying
|
||||||
|
-- those signatures. Once M of len(N) signatures are collected, tokens T
|
||||||
|
-- are subtracted from account 1 and given to account 4. Note that unlike
|
||||||
|
-- Rust, Lua is one-based and that account 1 is the first account.
|
||||||
|
|
||||||
|
local serialize = load(accounts[2].userdata)().serialize
|
||||||
|
|
||||||
|
function find(t, x)
|
||||||
|
for i, v in pairs(t) do
|
||||||
|
if v == x then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #accounts[3].userdata == 0 then
|
||||||
|
local cfg = load("return" .. data)()
|
||||||
|
accounts[3].userdata = serialize(cfg, nil, "s")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local cfg = load("return" .. accounts[3].userdata)()
|
||||||
|
local key = load("return" .. data)()
|
||||||
|
|
||||||
|
local i = find(cfg.n, key)
|
||||||
|
if i == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
table.remove(cfg.n, i)
|
||||||
|
cfg.m = cfg.m - 1
|
||||||
|
accounts[3].userdata = serialize(cfg, nil, "s")
|
||||||
|
|
||||||
|
if cfg.m == 0 then
|
||||||
|
accounts[1].tokens = accounts[1].tokens - cfg.tokens
|
||||||
|
accounts[4].tokens = accounts[4].tokens + cfg.tokens
|
||||||
|
|
||||||
|
-- End of game.
|
||||||
|
accounts[1].tokens = 0
|
||||||
|
accounts[2].tokens = 0
|
||||||
|
end
|
174
programs/native/solua/serialize.lua
Normal file
174
programs/native/solua/serialize.lua
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
----------------------------------------------------------------
|
||||||
|
-- serialize.lua
|
||||||
|
--
|
||||||
|
-- Exports:
|
||||||
|
--
|
||||||
|
-- orderedPairs : deterministically ordered version of pairs()
|
||||||
|
--
|
||||||
|
-- serialize : convert Lua value to string in Lua syntax
|
||||||
|
--
|
||||||
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
-- orderedPairs: iterate over table elements in deterministic order. First,
|
||||||
|
-- array elements are returned, then remaining elements sorted by the key's
|
||||||
|
-- type and value.
|
||||||
|
|
||||||
|
-- compare any two Lua values, establishing a complete ordering
|
||||||
|
local function ltAny(a,b)
|
||||||
|
local ta, tb = type(a), type(b)
|
||||||
|
if ta ~= tb then
|
||||||
|
return ta < tb
|
||||||
|
end
|
||||||
|
if ta == "string" or ta == "number" then
|
||||||
|
return a < b
|
||||||
|
end
|
||||||
|
return tostring(a) < tostring(b)
|
||||||
|
end
|
||||||
|
|
||||||
|
local inext = ipairs{}
|
||||||
|
|
||||||
|
local function orderedPairs(t)
|
||||||
|
local keys = {}
|
||||||
|
local keyIndex = 1
|
||||||
|
local counting = true
|
||||||
|
|
||||||
|
local function _next(seen, s)
|
||||||
|
local v
|
||||||
|
|
||||||
|
if counting then
|
||||||
|
-- return next array index
|
||||||
|
s, v = inext(t, s)
|
||||||
|
if s ~= nil then
|
||||||
|
seen[s] = true
|
||||||
|
return s,v
|
||||||
|
end
|
||||||
|
counting = false
|
||||||
|
|
||||||
|
-- construct sorted unseen keys
|
||||||
|
for k,v in pairs(t) do
|
||||||
|
if not seen[k] then
|
||||||
|
table.insert(keys, k)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(keys, ltAny)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- return next unseen table element
|
||||||
|
s = keys[keyIndex]
|
||||||
|
if s ~= nil then
|
||||||
|
keyIndex = keyIndex + 1
|
||||||
|
v = t[s]
|
||||||
|
end
|
||||||
|
return s, v
|
||||||
|
end
|
||||||
|
|
||||||
|
return _next, {}, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- avoid 'nan', 'inf', and '-inf'
|
||||||
|
local numtostring = {
|
||||||
|
[tostring(-1/0)] = "-1/0",
|
||||||
|
[tostring(1/0)] = "1/0",
|
||||||
|
[tostring(0/0)] = "0/0"
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(numtostring, { __index = function (t, k) return k end })
|
||||||
|
|
||||||
|
-- serialize: Serialize a Lua data structure
|
||||||
|
--
|
||||||
|
-- x = value to serialize
|
||||||
|
-- out = function to be called repeatedly with strings, or
|
||||||
|
-- table into which strings should be inserted, or
|
||||||
|
-- nil => return a string
|
||||||
|
-- iter = function to iterate over table elements, or
|
||||||
|
-- "s" to sort elements by key, or
|
||||||
|
-- nil for default (fastest)
|
||||||
|
--
|
||||||
|
-- Notes:
|
||||||
|
-- * Does not support self-referential data structures.
|
||||||
|
-- * Does not optimize for repeated sub-expressions.
|
||||||
|
-- * Does not preserve topology; only values.
|
||||||
|
-- * Does not handle types other than nil, number, boolean, string, table
|
||||||
|
--
|
||||||
|
local function serialize(x, out, iter)
|
||||||
|
local visited = {}
|
||||||
|
local iter = iter=="s" and orderedPairs or iter or pairs
|
||||||
|
assert(type(iter) == "function")
|
||||||
|
|
||||||
|
local function _serialize(x)
|
||||||
|
if type(x) == "string" then
|
||||||
|
|
||||||
|
out(string.format("%q", x))
|
||||||
|
|
||||||
|
elseif type(x) == "number" then
|
||||||
|
|
||||||
|
out(numtostring[tostring(x)])
|
||||||
|
|
||||||
|
elseif type(x) == "boolean" or
|
||||||
|
type(x) == "nil" then
|
||||||
|
|
||||||
|
out(tostring(x))
|
||||||
|
|
||||||
|
elseif type(x) == "table" then
|
||||||
|
|
||||||
|
if visited[x] then
|
||||||
|
error("serialize: recursive structure")
|
||||||
|
end
|
||||||
|
visited[x] = true
|
||||||
|
local first, nextIndex = true, 1
|
||||||
|
|
||||||
|
out "{"
|
||||||
|
|
||||||
|
for k,v in iter(x) do
|
||||||
|
if first then
|
||||||
|
first = false
|
||||||
|
else
|
||||||
|
out ","
|
||||||
|
end
|
||||||
|
if k == nextIndex then
|
||||||
|
nextIndex = nextIndex + 1
|
||||||
|
else
|
||||||
|
if type(k) == "string" and k:match("^[%a_][%w_]*$") then
|
||||||
|
out(k.."=")
|
||||||
|
else
|
||||||
|
out "["
|
||||||
|
_serialize(k)
|
||||||
|
out "]="
|
||||||
|
end
|
||||||
|
end
|
||||||
|
_serialize(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
out "}"
|
||||||
|
visited[x] = false
|
||||||
|
else
|
||||||
|
error("serialize: unsupported type")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local result
|
||||||
|
if not out then
|
||||||
|
result = {}
|
||||||
|
out = result
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(out) == "table" then
|
||||||
|
local t = out
|
||||||
|
function out(s)
|
||||||
|
table.insert(t,s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
_serialize(x)
|
||||||
|
|
||||||
|
if result then
|
||||||
|
return table.concat(result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
orderedPairs = orderedPairs,
|
||||||
|
serialize = serialize
|
||||||
|
}
|
@ -59,6 +59,9 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use solana_program_interface::account::{create_keyed_accounts, Account};
|
use solana_program_interface::account::{create_keyed_accounts, Account};
|
||||||
use solana_program_interface::pubkey::Pubkey;
|
use solana_program_interface::pubkey::Pubkey;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_accounts() -> Result<()> {
|
fn test_update_accounts() -> Result<()> {
|
||||||
@ -191,4 +194,105 @@ mod tests {
|
|||||||
assert_eq!(accounts[0].1.tokens, 100);
|
assert_eq!(accounts[0].1.tokens, 100);
|
||||||
assert_eq!(accounts[0].1.userdata, vec![]);
|
assert_eq!(accounts[0].1.userdata, vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_test_file(name: &str) -> Vec<u8> {
|
||||||
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
path.push(name);
|
||||||
|
let mut file = File::open(path).unwrap();
|
||||||
|
let mut contents = vec![];
|
||||||
|
file.read_to_end(&mut contents).unwrap();
|
||||||
|
contents
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_lua_library() {
|
||||||
|
let userdata = r#"
|
||||||
|
local serialize = load(accounts[2].userdata)().serialize
|
||||||
|
accounts[3].userdata = serialize({a=1, b=2, c=3}, nil, "s")
|
||||||
|
"#.as_bytes()
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
|
let program_id = Pubkey::default();
|
||||||
|
|
||||||
|
let alice_account = Account {
|
||||||
|
tokens: 100,
|
||||||
|
userdata,
|
||||||
|
program_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialize_account = Account {
|
||||||
|
tokens: 100,
|
||||||
|
userdata: read_test_file("serialize.lua"),
|
||||||
|
program_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut accounts = [
|
||||||
|
(Pubkey::default(), alice_account),
|
||||||
|
(Pubkey::default(), serialize_account),
|
||||||
|
(Pubkey::default(), Account::new(1, 0, program_id)),
|
||||||
|
];
|
||||||
|
let mut keyed_accounts = create_keyed_accounts(&mut accounts);
|
||||||
|
|
||||||
|
process(&mut keyed_accounts, &[]);
|
||||||
|
|
||||||
|
// Verify deterministic ordering of a serialized Lua table.
|
||||||
|
assert_eq!(
|
||||||
|
str::from_utf8(&keyed_accounts[2].account.userdata).unwrap(),
|
||||||
|
"{a=1,b=2,c=3}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lua_multisig() {
|
||||||
|
let program_id = Pubkey::default();
|
||||||
|
|
||||||
|
let alice_pubkey = Pubkey::new(&[0; 32]);
|
||||||
|
let serialize_pubkey = Pubkey::new(&[1; 32]);
|
||||||
|
let state_pubkey = Pubkey::new(&[2; 32]);
|
||||||
|
let bob_pubkey = Pubkey::new(&[3; 32]);
|
||||||
|
let carol_pubkey = Pubkey::new(&[4; 32]);
|
||||||
|
let dan_pubkey = Pubkey::new(&[5; 32]);
|
||||||
|
let erin_pubkey = Pubkey::new(&[6; 32]);
|
||||||
|
|
||||||
|
let alice_account = Account {
|
||||||
|
tokens: 100,
|
||||||
|
userdata: read_test_file("multisig.lua"),
|
||||||
|
program_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialize_account = Account {
|
||||||
|
tokens: 100,
|
||||||
|
userdata: read_test_file("serialize.lua"),
|
||||||
|
program_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut accounts = [
|
||||||
|
(alice_pubkey, alice_account), // The payer and where the program is stored.
|
||||||
|
(serialize_pubkey, serialize_account), // Where the serialize library is stored.
|
||||||
|
(state_pubkey, Account::new(1, 0, program_id)), // Where program state is stored.
|
||||||
|
(bob_pubkey, Account::new(1, 0, program_id)), // The payee once M signatures are collected.
|
||||||
|
];
|
||||||
|
let mut keyed_accounts = create_keyed_accounts(&mut accounts);
|
||||||
|
|
||||||
|
let data = format!(
|
||||||
|
r#"{{m=2, n={{"{}","{}","{}"}}, tokens=100}}"#,
|
||||||
|
carol_pubkey, dan_pubkey, erin_pubkey
|
||||||
|
).as_bytes()
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
|
process(&mut keyed_accounts, &data);
|
||||||
|
assert_eq!(keyed_accounts[3].account.tokens, 1);
|
||||||
|
|
||||||
|
let data = format!(r#""{}""#, carol_pubkey).into_bytes();
|
||||||
|
process(&mut keyed_accounts, &data);
|
||||||
|
assert_eq!(keyed_accounts[3].account.tokens, 1);
|
||||||
|
|
||||||
|
let data = format!(r#""{}""#, dan_pubkey).into_bytes();
|
||||||
|
process(&mut keyed_accounts, &data);
|
||||||
|
assert_eq!(keyed_accounts[3].account.tokens, 101); // Pay day!
|
||||||
|
|
||||||
|
let data = format!(r#""{}""#, erin_pubkey).into_bytes();
|
||||||
|
process(&mut keyed_accounts, &data);
|
||||||
|
assert_eq!(keyed_accounts[3].account.tokens, 101); // No change!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user