Calling The Generic Multisig In Pure Shell
This is a quite low-level step-by-step example of calling a contract with
arguments from a generic-multisig contract — hence building a Michelson lambda,
serializing it, and signing it. All of it in pure (POSIX) shell with
tezos-client
.
There are higher-level, and likely more usable versions, like
github.com/TessellatedGeometry/multisig-command-compiler
using SmartPy, or the merge-request
tezos/tezos!1857
adding commands to tezos-client
.
The full shell script is in the following gist:
gist.github.com/smondet/0ea3f22375a309892fa5855cb7d7c1d2
,
it includes the Markdown prose as comments too.
⁂
Let's setup a silent sandbox,
and configure tezos-client
for it:
docker run --rm --name my-sandbox --detach -p 20000:20000 \
tqtezos/flextesa:20201214 delphibox start
tezos-client --endpoint http://localhost:20000 config update
tezos-client import secret \
key alice unencrypted:edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq --force
tezos-client import secret key \
bob unencrypted:edsk3RFfvaFaxbHx8BMtEW1rKQcPtDML3LXjNqMNLCzC3wLC1bWbAt --force
(don't forget to docker kill my-sandbox
when you're done).
This is the contract that we want to call from the multisig:
it just stores the sender and the argument it is called with, let's
call it “target
”:
where:
alice
is the whale account from the sandbox, andNone
is the initialization of the storage.
The multisig contract we want to use is there:
Check-out also generic_multisig.v, its formalization and correctness proofs in Coq.
Let's download it:
And originate it:
where:
- The storage looks like:
(Pair <initialize-counter> (Pair <signature-threshold> {<public-keys>}))
. - We set the threshold to
1
for simplicity. "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn"
isbob
's public key (does not have to have any balance on chain, it is just a signer).
Now the fun part, let's build, pack, and sign a Michelson expression that instructs the multisig to call the “target” contract.
We need Base58 (“KT1”) addresses, because we cannot just use
tezos-client
aliases in Michelson expressions:
The first time we “know” that the replay-protection counter is 0:
But in general we want to get it from the contract storage, so,
very elegantly, we sed
it out:
OK, here is the meat^Wprotein — a Michelson expression of type
(lambda unit (list operation))
:
cat > /tmp/lambda.tz <<EOF
{
DROP; # This is the Unit value the lambda is called on.
# We build a list with CONS, so we start with the empty one:
NIL operation;
# One call to TRANSFER_TOKENS to build an operation:
{ # ← this pair of braces is just for esthetics.
PUSH address "$target_kt1"; # The target address,
CONTRACT string; # transformed into a contract of target's type.
ASSERT_SOME; # CONTRACT returns an option, we want the value.
PUSH mutez 0; # The transfer amount is 0 mutez.
PUSH string "hello-$counter"; # The argument passed to the target contract.
TRANSFER_TOKENS;
};
CONS; # Finally, we build the list of one operation, leave it on the stack.
}
EOF
We remove the comments, and “flatten” all of the above to avoid
dealing with tezos-client
's extreme pedantism about the indentation
of Michelson:
The argument passed to the multisig's main
entrypoint is the
counter + the lambda as an action
(the action is of type (or <action> <change-keys>)
):
To avoid replay attacks on the test-chain during the 3rd voting period, the multisig also requires the chain-id to be signed:
The thing to serialize and sign is hence, the chain-id, the contract address, and the payload:
You can check that echo "$topack"
shows something like:
(Pair (Pair "NetXMFJWfpUBox7" "KT1Mfv7qCR9zfQZJcG8Bx7n6XygiWN3fHVNG") (Pair 1 (Left {DROP; NIL operation;{PUSH address "KT1KT6smv2ivadEMPiYS2fDWVKdApsVpffts"; CONTRACT string; ASSERT_SOME; PUSH mutez 0; PUSH string "hello-1"; TRANSFER_TOKENS;};CONS; })))
(here NetXMFJWfpUBox7
is the vanity chain-id of the sandbox).
Now we serialize this. We need to give the type of the expression
but we can cheat a bit: since we only use the left side of the (or _ _)
we can put unit
on the right-side, feel free to copy the real type from
generic.tz
:
The command tezos-client hash data
throws a lot of output,
we only care about the line that looks like:
Raw packed data: 0x050707070……
We grab the hexadecimal blob into a file and feed it to the signer:
The output is the Base58check-encoded signature, exactly what we need to build a Michelson literal to call the contract:
We see that alice
passes the payload together with a list of
(option signature)
values,
in our case a singleton corresponding to bob
's signature.
If everything goes as planned, we can see in the target-contract's
storage that the address of the multisig has been recorded:
(should return the same as
echo "Some (Pair \"$msig_kt1\" \"hello-$counter\")"
).
After 8 years of blograstination, this is post #11 of my attempt at using the #100DaysToOffload
completely unrealistic “challenge” to remind me to write stuff once in a while … Let's see where this goes.