1
0
mirror of https://github.com/danog/toncontest.git synced 2024-11-26 12:04:50 +01:00
This commit is contained in:
Daniil Gentili 2019-10-15 16:17:12 +02:00
parent 496cd2279d
commit d826632e3f
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
26 changed files with 2603 additions and 43 deletions

202
lightning/README.md Normal file
View File

@ -0,0 +1,202 @@
# Multisignature wallet
Daniil Gentili's submission (@danogentili, <daniil@daniil.it>).
Upgradable multisignature wallet, with custom scripts to deserialize and inspect the contents of BOC files containing TL-B Message constructors, create, sign and verify wallet requests and wallet code upgrades.
All custom data structures used in the wallet can be viewed as a custom TL-B scheme in `proto/scheme.tlb` (some basic TON constructors are also included for reference).
Most smart contact get-methods (except for the basic seqno and getPartials methods) return an integer, indicating whether the operation was successful and the requested data was found, followed by a cell/integer with the found data (or an empty cell/0 in case of failure).
I created a full testing platform with a collator emulator in FIFT that parses the generated external messages and runs the required logic (including code and data initialization from the StateInit of constructor messages, with support for multiple consecutive method calls and data persistence) in the TVM, partially emulating the logic of `Transaction::unpack_input_msg` and `Collator::create_ordinary_transaction`, also allowing us to run get-methods.
I've tested throughly the smart contract using my local collator emulator (`test.fif`), and the actual TON collator using a slightly tweaked [zerostate generator](https://github.com/ton-blockchain/ton/pull/145) and the custom `test-collator.sh` script.
I had loads of fun working with funC and especially fift (after the first moment of confusion, I really enjoyed working with it due to the absolute control on the memory it allows), and I'm really looking forward to building stuff on TON; especially TON services on the P2P ADNL network, I'll start by adding support for the ADNL protocol in my MTProto client, [MadelineProto](https://github.com/danog/MadelineProto); but I'll also experiment more with the TON blockchain, creating s'more smart contracts.
## Project structure
Fift scripts:
* `lib.fif` - Library with functions to deserialize and inspect contents of TON messages
The external TL-B Message X object is deserialized, with info printed to stdout.
Then, the custom `multiSigWrapper`, `wrappedMessage`, `modeMessage` and `codeMessage` TL-B objects are deserialized, with info printed to stdout.
In the case of `modeMessage` (simple message to be sent by wallet once enough signatures are gathered), a the internal Message X object is also unpacked, with info printed to stdout.
In the case of `codeMessage`, the new public key dictionary is unpacked and printed to stdout, along with a csr dump of the code slice and other info.
* `test.fif` - TVM testing platform (collator emulator)
```
usage: test.fif <init-message> <message> <func> [ <message2> <func2> ... ]
```
Runs the function with ID <func> in the TON VM, using initial code and storage data from <init-message>.boc and <message>.boc as message (OR as the only integer parameter of get-method, if <message>.boc does not exist).
The TVM persistent storage is dumped to console and reused after each method call, allowing detailed debugging of smart contracts, with multiple method calls altering the contract's persistent storage.
* `gen-pub.fif` - Generates public/private keypair
```
usage: gen-pub.fif <privkey>
```
Create public key files from private keys; if `<privkey>` doesn't exist, it will be created.
Will also print the hex public key ID.
* `wallet-create.fif` - Creates shared wallet
```
usage: wallet-create.fif <workchain-id> <wallet-name> <n> <k> <privkey1> [<pubkey2> ...]
```
Creates a new multisignature wallet in specified workchain composed of `<n>` (1-10) keys.
The first of the keys must be a private key (pre-existing or not), used to generate the wallet; the rest MUST be public keys.
Min `<k>` (1-10) signatures required to send an order; load `<n>` pre-existing public keys from files `<key1...n>`.
* `wallet-update.fif` - Creates wallet code update request
```
usage: wallet-update.fif <wallet-name> <wallet-update> <seqno> <n> <k> <key-id> <privkey1> [<pubkey2> ...]
```
Updates an existing multisignature wallet.
The first of the keys must be a private key (with ID `<key-id>`), used to sign the wallet update request saved to `<wallet-update>.boc`; the rest MUST be public keys.
Create or generate public key files from private keys using gen-pub.fif privkey
Min `<k>` (1-10) signatures required to send an order; load `<n>` pre-existing public keys from files `<key1...n>`.
* `create.fif` - Creates simple message to be sent to wallet
```
usage: create.fif <filename-base> <key> <key-id> <dest-addr> <seqno> <amount> [-B <body-boc>] [<savefile>]
```
Creates a request to shared wallet created by wallet-create.fif, with private key `<key-id>` loaded from file `<key>.pk` and address from `<filename-base>.addr`, and saves it into `<savefile>.boc` ('wallet-query.boc' by default)
* `sign.fif` - Adds signature to wallet message
```
usage: sign.fif <input> <output> <key> <key-id>
```
Signs multisig `<input>.boc` file with private key `<key-id>` loaded from file `<key>.pk` and writes result to `<output>.boc`
* `verify.fif` - Verifies signature of message against known public key(s)
```
usage: verify.fif <input> <key1> <key1-id> [<key2> <key2-id> ...]
```
Verify multisig `<input>.boc` file with public key `<keyN-id>` loaded from file `<keyN>.pubkey`
* `merge.fif` - Merges multiple messages with same content and different signatures
```
usage: merge.fif <input1> <input2> [<input3> ...] <output>
```
Merges multisig `<inputx>.boc` files and writes result to `<output>.boc`
* `inspect.fif` - Inspects the contents of request
```
usage: inspect.fif <input>
```
Inspects contents of multisig `<input>.boc` file.
FunC code:
* `wallet-code.fc` - Wallet code
* `wallet-code-update.fc` - Wallet code with minor change to test wallet code upgrade
Scripts:
* `test.sh` - Creates wallet, set of signed requests and tests wallet in TON VM
* `test-update.sh` - Tests wallet code upgrade functionality (after `test.sh`) in TON VM
## Testing
The `test.sh` script contains a full set of commands to test every fift script.
You might want to run each command inside `test.sh` separately (there are also multiple comments explaining what each line does in detail).
What follows is a line-by-line description of the actions in `test.sh`.
The script starts by creating ten public/private keypairs using `gen-pub.fif`:
```
for f in {a..j}; do fift -s ../gen-pub.fif $f;done
```
Next, the script creates a constructor message for the shared wallet smart contract using `wallet-create.fif`, creating a `pony-create.boc` file:
```
fift -s ../wallet-create.fif 0 pony 10 10 {a..j} | tee log
```
After extracting the computed wallet address from `log`, the scripts generates an initial wallet request, requesting to send 10 grams to itself (just as a test, saving to `a.boc`).
```
fift -s ../create.fif pony a 0 $address 0 10 a
```
Here, note the `a 0`: to save space, I have chosen to not use the entire ecdh key as key in the signature dictionary.
Instead, a **key ID** is used to distinguish signatures made by certain keys: this is a simple 4-bit value (instead of 256 bits!), equal to the position of the key in the `wallet-create` argument list.
In this case, the `a` key was the first key (`{a..j}` in Bash is shorthand for `a b c .. j`), so the ID is `0`.
What follows is the address, the seqno, the amount of grams and the savefile (`a`) for the query.
Some helper Bash functions are defined:
```
chr() { [ "$1" -lt 256 ] || return 1; printf "\\$(printf '%03o' "$1")"; }
ord() { LC_CTYPE=C printf '%d' "'$1"; }
```
Sign the query using all keys separately, creating eight more boc files, each signed by TWO keys only (key `a` and key `{b..j}`):
```
for f in {1..9}; do fift -s ../sign.fif a $(chr $((97+f))) $(chr $((97+f))) $f;done
```
Merge all queries:
```
fift -s ../merge.fif {a..j} merge
```
Inspect the merged query:
```
fift -s ../inspect.fif merge
```
Finally run the generated files in the VM in the following order
* First init VM with constructor message
* Then load the first file with only one signature by key a (ID 0, method -1)
* Run seqno get-method (method 85143)
* Run getPartialsByKeyId get-method (method 113609)
* Load file with all signatures (and send message, method -1)
* Run getPartialsByKeyId get-method (method 113609)
```
fift -s ../test.fif \
pony-create \
a -1 \
0 85143 \
0 113609 \
merge -1 \
0 113609
```
## Upgrading code
`test-update.sh` should be run after `test.sh`, it executes a similar set of operations:
* The wallet is initialized as before (**10 signatures required** to send message)
* A simple message is created with only 1 signature (`a`), sending 10 grams to the wallet itself
* This message is sent to the wallet and stored, waiting for further (9 more) signatures
* A **code upgrade** message is created with only 1 signature (`a`): the upgrade sets to 3 the minimum number of required signatures
* This message is sent to the wallet and stored, waiting for further (9 more) signatures
* 9 more signatures (`b-j`) are appended to the **code upgrade** message
* The full **code upgrade** message is sent to the wallet, setting to **3** the minimum number of signatures required to send the message, also modifying the code by adding a new method.
This code upgrade message, however, was signed by all **10** users, so it's really just a simpler way to upgrade a wallet: instead of creating new wallet => moving all funds to new upgraded wallet, a simple code upgrade message is signed.
* 2 more signatures (`b`, `c`) are appended to the **simple** message
* The simple message with additional signatures (`b`, `c`) is sent to the wallet, sending out wallet funds since all **3 required signatures** are gathered.
If we were running on the blockchain, at this point the smart contract code root would be updated to point to the new code, allowing us to use the new `getMagic` (77784) method that returns (420, 69) when called.
Since we're running in a local TVM instance, and fift's `runvm` primitives have no way of returning the output action list (including changes to the code), the code isn't actually updated, but the modified signature list and minSig data structures in the persistent contract storage are indeed returned to the fift stack and re-used for the next TVM method call, allowing us to test the feature.
## Collator testing
After running `test.sh` and `test-update.sh` (thus generating all BOC files), you can run the `test-collator.sh` script, to:
* Automatically configure a TON collator instance
* Make a query to the testgiver on the local collator instance
* Load the wallet smart contract
* Load a query with 2 signatures (`a`, `b`)
* Upgrade the smart contract to accept 3 signatures
* Load one more signature and send the message

92
lightning/create.fif Executable file
View File

@ -0,0 +1,92 @@
#!/usr/bin/env -S fift -s
"TonUtil.fif" include
{
."usage: " @' $0 type ." <filename-base> <key> <key-id> <dest-addr> <seqno> <amount> [-B <body-boc>] [<savefile>]" cr
."Creates a request to shared wallet created by wallet-create.fif, with private key <key-id> loaded from file <key>.pk "
."and address from <filename-base>.addr, and saves it into <savefile>.boc ('wallet-query.boc' by default)" cr 1 halt
} : usage
def? $8 { @' $7 "-B" $= { @' $8 =: body-boc-file [forget] $8 def? $9 { @' $9 =: $7 [forget] $9 } { [forget] $7 } cond
@' $# 2- =: $# } if } if
$# 6 < $# 9 > or ' usage if
true constant bounce
$1 =: file-base
$2 =: key
$3 parse-int =: key-id
$4 bounce parse-load-address =: bounce 2=: dest_addr
$5 parse-int =: seqno
$6 $>GR =: amount
def? $7 { @' $7 } { "wallet-query" } cond constant savefile
3 constant send-mode // mode for SENDRAWMSG: +1 - sender pays fees, +2 - ignore errors
3 86400 * constant timeout // external message expires in 3 days
file-base +".addr" load-address
2dup 2constant wallet-addr
."Source wallet address = " 2dup .addr cr 6 .Addr cr
key +".pk" load-keypair nip constant wallet_pk
def? body-boc-file { @' body-boc-file file>B B>boc } { <b 0 32 u, "TESTv2" $, b> } cond
constant body-cell
."Transferring " amount .GR ."to account "
dest_addr 2dup bounce 7 + .Addr ." = " .addr
."seqno=0x" seqno x. ."bounce=" bounce . cr
."Body of transfer message is " body-cell <s csr. cr
// create a message
<b b{01} s, bounce 1 i, b{000100} s, dest_addr addr, amount Gram, 0 9 64 32 + + 1+ u,
body-cell <s 2dup s-fits? not rot over 1 i, -rot { drop body-cell ref, } { s, } cond
b>
// create wrapper message
// modeMessage$0 mode:uint8 body:^(Message X) = ModeMessage X;
// wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X;
<b now timeout + 32 u, seqno 32 u, 0 1 u, send-mode 8 u, swap ref, b>
dup ."signing message: " <s csr. cr
dup hashu
dup ."Hash: " x. cr cr
wallet_pk ed25519_sign_uint
// signature$_ R:bits256 s:bits256 = Signature;
256 B>u@+ swap 256 B>u@ swap
<b swap 256 u, swap 256 u, b>
// key ID => signature
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
<b
0 1 u, // $0
swap <s key-id dictnew 4 udict! // Create and populate dictionary
not abort"Failure inserting signature!"
dict, // signatures
swap <s s, // message
b>
//
// addr_none$00 = MsgAddressExt;
// addr_extern$01 len:(## 9) external_address:(bits len)
// = MsgAddressExt;
// anycast_info$_ depth:(#<= 30) { depth >= 1 }
// rewrite_pfx:(bits depth) = Anycast;
// addr_std$10 anycast:(Maybe Anycast)
// workchain_id:int8 address:bits256 = MsgAddressInt;
// addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9)
// workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
//
// ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
// import_fee:Grams = CommonMsgInfo;
//
//
// message$_ {X:Type} info:CommonMsgInfo
// init:(Maybe (Either StateInit ^StateInit))
// body:(Either X ^X) = Message X;
//
<b b{1000100} s, wallet-addr addr, 0 Gram, b{00} s,
swap <s s, b>
dup ."resulting external message: " <s csr. cr
2 boc+>B dup Bx. cr
savefile +".boc" tuck B>file
."Query expires in " timeout . ."seconds" cr
."(Saved to file " type .")" cr

15
lightning/gen-pub.fif Normal file
View File

@ -0,0 +1,15 @@
"TonUtil.fif" include
{ ."usage: " @' $0 type ." <privkey>" cr
."Create public key files from private keys; if <privkey> doesn't exist, it will be created." cr cr
1 halt
} : usage
$# 1 < ' usage if
$1 +".pk" load-generate-keypair drop
dup $1 +".pubkey" B>file
."Wrote private key to " $1 +".pk" type cr
."Wrote public key to " $1 +".pubkey" type cr cr
."Public key: " Bx. cr cr

9
lightning/inspect.fif Normal file
View File

@ -0,0 +1,9 @@
{
."usage: " @' $0 type ." <input>" cr
."Inspects contents of multisig <input>.boc file." cr 1 halt
} : usage
$# 1 < ' usage if
"lib.fif" include
$1 +".boc" load-boc inspect

235
lightning/lib.fif Normal file
View File

@ -0,0 +1,235 @@
"TonUtil.fif" include
// b body -- b'
{ tuck <s 2dup s-fits? not rot over 1 i, -rot
{ drop swap ref, } { s, nip } cond
} : maybe-ref,
// filename -- c
{ dup ."Loading order from file " type ."..." cr
file>B B>boc
} : load-boc
// filename -- uint256
{ dup ."Loading public key from file " type ."..." cr
file>B dup Blen 32 <> abort"Public key must be exactly 32 bytes long"
256 B>u@
} : load-pubkey
' constant : const
' 2constant : 2const
// D n -- uint
{
0 -rot
{ drop swap 1+ swap -1 } dictmap drop
} : dictlen
// u D n -- s ? or ?
{
{ swap 2 pick = { nip 0 } { drop -1 } cond } dictforeach
dup ' nip if
not
} : dict[]
{ hole dup 1 ' @ does create 1 ' ! does create } : variable-set
{ hole hole 2dup 2 { @ swap @ swap } does create 2 { rot swap ! ! } does create } : 2variable-set
2variable-set wallet-addr wallet-addr!
variable-set message-hash message-hash!
variable-set message-contents message-contents!
variable-set sig-count sig-count!
// udata BSig ukey ?
{
256 u>B rot 256 u>B -rot ed25519_chksign
} : ed25519_chksignuu
// Inspect multisigwrapper starting from signatures
//
// multiSigWrapper$_ signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapperStorage X;
//
// s --
{
dict@+ swap
dup null? abort"Empty signature list!"
dup 4 dictlen sig-count!
."Signed by the following keys: "
4 { drop . ."- " -1 } dictforeach cr drop
."Hash: " dup s>c hashu dup x. cr
message-hash!
// modeMessage$0 mode:uint8 body:^(Message X) = ModeMessage X;
// wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X;
32 u@+ swap
dup ."Expires: " .
dup now < { ."(already EXPIRED!)" drop } { ."(in " now - . ."seconds)" } cond cr
32 u@+ swap
."Seqno: " . cr
1 u@+ swap
{
."Is code message!" cr
// codeMessage$1 minSigs:(## 4) keys:(HashmapE 4 PubKey) code:^Cell = ModeMessage X;
4 u@+ swap
."Minsigs: " . cr
dict@+ swap
dup null? abort"Empty key list!"
."The following keys are present: " cr
4 { swap ."* " . ."- " 32 B@ Bx. cr -1 } dictforeach cr drop
ref@
."Code: " <s csr.
}
{
8 u@+ swap
."Mode: " . cr
// Now on to the actual message we're agreeing to sign
//
// int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
// src:MsgAddressInt dest:MsgAddressInt
// value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
// created_lt:uint64 created_at:uint32 = CommonMsgInfo;
// ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
// import_fee:Grams = CommonMsgInfo;
// ext_out_msg_info$11 src:MsgAddressInt dest:MsgAddressExt
// created_lt:uint64 created_at:uint32 = CommonMsgInfo;
."=>" cr ref@ <s
1 u@+ swap
{ // External message *$1*
."Inside: external message" cr
}
{ // Internal message int_msg_info$0
."Inside: internal message" cr
1 u@+ swap
."Instant hypercube routing disabled? " . cr
1 u@+ swap
."Bounce flag set? " . cr
1 u@+ swap
// ."Bounced flag set? " . cr
drop
2 u@+ nip // Drop src address constructor + flags
3 u@+ swap // Read dst address constructor + flags
// addr_std$10 anycast 0 => 100 => 4
4 <> abort"Unsupported address!" // Make things simple for now
8 i@+
256 u@+ -rot
."Destination address: " .addr cr
Gram@+ swap
."Grams: " .GR cr
} cond
drop
} cond
} : inspect-multisig
// Inspects contents of cell provided on top of the stack
// c --
//
// defines/updates the following vars
//
// wallet-addr = u u
// message-hash = B
// message-contents = s
{
// Assuming the same external structure created by create.fif (simple external message)
//
// addr_none$00 = MsgAddressExt;
// addr_extern$01 len:(## 9) external_address:(bits len)
// = MsgAddressExt;
// anycast_info$_ depth:(#<= 30) { depth >= 1 }
// rewrite_pfx:(bits depth) = Anycast;
// addr_std$10 anycast:(Maybe Anycast)
// workchain_id:int8 address:bits256 = MsgAddressInt;
// addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9)
// workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
//
// ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
// import_fee:Grams = CommonMsgInfo;
//
//
// message$_ {X:Type} info:CommonMsgInfo
// init:(Maybe (Either StateInit ^StateInit))
// body:(Either X ^X) = Message X;
//
// <b b{1000100} s, wallet-addr addr, 0 Gram, b{00} s,
// swap <s s, b>
// (external message)
//
// message$_ {X:Type} info:CommonMsgInfo
// init:(Maybe (Either StateInit ^StateInit))
// body:(Either X ^X) = Message X;
<s dup csr. cr
// External message
// ext_in_msg_info$10 src:MsgAddressExt (addr_none$00) dest:MsgAddressInt (addr_std$10 anycast 0) ... import_fee:Grams = CommonMsgInfo;
// 10 00 10 0
// 1000100 => 68
7 u@+ swap 68 <> { ."There seems to be an invalid header" cr } if
8 i@+
256 u@+ -rot
2dup wallet-addr!
."Wallet address: " .addr cr
Gram@+ nip // Ignore grams
1 u@+ swap // init:(Maybe
abort"This seems to be an init message"
// body:(Either X ^X)
1 u@+ swap
{ ref@ <s } if // Load ref
dup message-contents!
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
1 u@+ swap
dup ."Message version: " . cr
abort"Unsupported message version!"
inspect-multisig
} : inspect
// storage$_ seqno:uint32 minSigs:(## 4) keys:(HashmapE 4 PubKey) messages:(HashmapE 256 (MultiSigWrapperStorage X)) = Storage X;
// c --
{
<s
32 u@+ swap
."Seqno: " . cr
4 u@+ swap
."Minsigs: " . cr
dict@+ swap
."Keys: " 4 dictlen . cr
dict@
."Messages: " dup 256 dictlen . cr
256
{
cr ."Inspecting message " swap x. cr
4 u@+ swap
."Signature count: " dup . cr
sig-count!
inspect-multisig
-1
} dictforeach drop
} : inspect-storage

53
lightning/merge.fif Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env -S fift -s
"TonUtil.fif" include
"lib.fif" include
{
."usage: " @' $0 type ." <input1> <input2> [<input3> ...] <output>" cr
."Merges multisig <inputx>.boc files and writes result to <output>.boc" cr 1 halt
} : usage
$# 3 < ' usage if
$# 1 - const input-len
1 { dup $() +".boc" swap 1+ } input-len times drop
input-len tuple const input-files
$# $() +".boc" const output-file
."Inspecting and merging " input-len . ."files..." cr
input-files explode 1- swap
load-boc inspect cr
message-hash const previous-hash
wallet-addr 2const previous-address
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
message-contents
1 u@+ nip // $0 was already verified
dict@+ // Read signatures
s>c const wrapped-message // Store rest of message in const
swap // put length on top, signatures right underneath
{ swap // Get filename, signatures right underneath
load-boc inspect cr
message-hash previous-hash <> abort"Hash mismatch!"
// I suppose it's ok if there are multiple wallet instances with the same messages on different workchains/at different addresses, we're all agreeing to sign the same message anyway
// wallet-address previous-address rot <> abort"Wallet address mismatch!" <> abort"Wallet address mismatch (different workchain)!"
message-contents
1 u@+ nip // $0 was already verified
dict@ // Read signatures
4 { drop s, -1 } dictmerge
} swap times
// Signatures are the only element on the stack
."Final message signed by the following keys: "
dup 4 { drop . ."- " -1 } dictforeach cr drop
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
<b 0 1 u, swap dict, wrapped-message <s s, b>
// Renerate external message
<b 68 7 u, wallet-addr addr, 0 Gram, 0 1 u, swap maybe-ref, b>
2 boc+>B dup Bx. cr
output-file tuck B>file
."Saved new multisigned message to file " type cr

55
lightning/proto/old.fif Normal file
View File

@ -0,0 +1,55 @@
// Some old code I wrote for serializing the smart contract key database in a more compact manner, hoping to save grams on storage
// This was scrapped and substituted with a simple dictionary after my tests showed that the code was consuming three times the amount of gas of a simple load dictionary primitive.
// Keys will be deserialized to a simple tuple, using the correct PubKeys constructor depending on the number n of keys left
//
// pubKeys1$_ key1:PubKey = PubKeys;
// pubKeys2$_ key1:PubKey key2:PubKey = PubKeys;
// pubKeys3$_ key1:PubKey key2:PubKey key3:PubKey = PubKeys;
// pubKeys3and$_ key1:PubKey key2:PubKey key3:PubKey _:^PubKeys = PubKeys;
//
// keys:[ ^PubKeys ]
// First create builders with groups of (at most) 3 keys each
<b
0 { dup 1+ swap // Create a counter
// Get builder b (or create a new one)
// if ($k && !($k % 3)) {
dup dup 3 mod 0<> not and { <b } { 2 roll } cond
keys rot [] // Get n-th value x
256 // y
u, // Write uint
swap
} n times drop
// Then convert builders into cells, appropriately inserting references for nested pubkeys
n 3 /c const nCeil // Number of builders
nCeil 1- const nRev // Steps for reverse counter
nCeil 3 > const shouldRef // Whether there are more than 3 builders and references must be created
nCeil 1+ const nRoll // Steps for roll
nRev { dup 1- swap // Create a reverse counter
rot // Get current builder
swap
// if ($x != $nRev && $x > 1 && $nCeil > 3)
dup nRev <> swap 1 > shouldRef and and { nCeil pick ref, } if
b> // Close builder, create cell
nCeil -roll // Put cell at the back of the stack
} nCeil times drop
// Number of cells to store / Number of already referenced cells to delete
nCeil 3 > { 3 nCeil 3 - } { nCeil 0 } cond const nRem const nCells
// Drop all cells we already referenced
' drop nRem times
// Reverse order of cells
nCells 0 reverse
// Create tuple with cells
nCells tuple const keyCells

183
lightning/proto/scheme.tlb Normal file
View File

@ -0,0 +1,183 @@
// This is just an approximated TL scheme, explaining the message formats used by the multisig wallet.
// Multisig wallet constructors
// 256 bits
pubKey$_ k:bits256 = PubKey;
// 512 bits
signature$_ R:bits256 s:bits256 = Signature;
// Count with refs
twoSignatures$_ a:^Signature b:^Signature = MaybeTwoSignatures;
oneSignatureA$0 a:^Signature = MaybeTwoSignatures;
oneSignatureB$1 b:^Signature = MaybeTwoSignatures;
// key ID => signature
incomingState$_ signatures:MaybeTwoSignatures seqno:uint32 delta:Grams final:Bool {turnB:(seqno % 2)} = Incoming;
incomingGrams$_ signature:Signature transferToA:Bool = IncomingInternal;
// For internal storage
state$_ signatures:MaybeTwoSignatures seqno:uint32 delta:Grams final:Bool {turnB:(seqno % 2)} = State;
// Storage
storage$_ keyA:PubKey keyB:PubKey amountA:Grams amountB:Grams state:State = Storage X;
// TON stuff
unit$_ = Unit;
true$_ = True;
// EMPTY False;
bool_false$0 = Bool;
bool_true$1 = Bool;
bool_false$0 = BoolFalse;
bool_true$1 = BoolTrue;
nothing$0 {X:Type} = Maybe X;
just$1 {X:Type} value:X = Maybe X;
left$0 {X:Type} {Y:Type} value:X = Either X Y;
right$1 {X:Type} {Y:Type} value:Y = Either X Y;
pair$_ {X:Type} {Y:Type} first:X second:Y = Both X Y;
bit$_ (## 1) = Bit;
/*
*
* FROM hashmap.tlb
*
*/
// ordinary Hashmap / HashmapE, with fixed length keys
//
hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n)
{n = (~m) + l} node:(HashmapNode m X) = Hashmap n X;
hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;
hmn_fork#_ {n:#} {X:Type} left:^(Hashmap n X)
right:^(Hashmap n X) = HashmapNode (n + 1) X;
hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m;
hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m;
unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);
hme_empty$0 {n:#} {X:Type} = HashmapE n X;
hme_root$1 {n:#} {X:Type} root:^(Hashmap n X) = HashmapE n X;
extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32))
= ExtraCurrencyCollection;
// true#_ = True;
_ {n:#} _:(Hashmap n True) = BitstringSet n;
// HashmapAug, hashmap with an extra value
// (augmentation) of type Y at every node
//
ahm_edge#_ {n:#} {X:Type} {Y:Type} {l:#} {m:#}
label:(HmLabel ~l n) {n = (~m) + l}
node:(HashmapAugNode m X Y) = HashmapAug n X Y;
ahmn_leaf#_ {X:Type} {Y:Type} extra:Y value:X = HashmapAugNode 0 X Y;
ahmn_fork#_ {n:#} {X:Type} {Y:Type} left:^(HashmapAug n X Y)
right:^(HashmapAug n X Y) extra:Y = HashmapAugNode (n + 1) X Y;
ahme_empty$0 {n:#} {X:Type} {Y:Type} extra:Y
= HashmapAugE n X Y;
ahme_root$1 {n:#} {X:Type} {Y:Type} root:^(HashmapAug n X Y)
extra:Y = HashmapAugE n X Y;
// VarHashmap / VarHashmapE, with variable-length keys
//
vhm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n)
{n = (~m) + l} node:(VarHashmapNode m X)
= VarHashmap n X;
vhmn_leaf$00 {n:#} {X:Type} value:X = VarHashmapNode n X;
vhmn_fork$01 {n:#} {X:Type} left:^(VarHashmap n X)
right:^(VarHashmap n X) value:(Maybe X)
= VarHashmapNode (n + 1) X;
vhmn_cont$1 {n:#} {X:Type} branch:Bit child:^(VarHashmap n X)
value:X = VarHashmapNode (n + 1) X;
// nothing$0 {X:Type} = Maybe X;
// just$1 {X:Type} value:X = Maybe X;
vhme_empty$0 {n:#} {X:Type} = VarHashmapE n X;
vhme_root$1 {n:#} {X:Type} root:^(VarHashmap n X)
= VarHashmapE n X;
//
// PfxHashmap / PfxHashmapE, with variable-length keys
// constituting a prefix code
//
phm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n)
{n = (~m) + l} node:(PfxHashmapNode m X)
= PfxHashmap n X;
phmn_leaf$0 {n:#} {X:Type} value:X = PfxHashmapNode n X;
phmn_fork$1 {n:#} {X:Type} left:^(PfxHashmap n X)
right:^(PfxHashmap n X) = PfxHashmapNode (n + 1) X;
phme_empty$0 {n:#} {X:Type} = PfxHashmapE n X;
phme_root$1 {n:#} {X:Type} root:^(PfxHashmap n X)
= PfxHashmapE n X;
/*
*
* END hashmap.tlb
*
*/
// TON messages
addr_none$00 = MsgAddressExt;
addr_extern$01 len:(## 9) external_address:(bits len)
= MsgAddressExt;
anycast_info$_ depth:(#<= 30) { depth >= 1 }
rewrite_pfx:(bits depth) = Anycast;
addr_std$10 anycast:(Maybe Anycast)
workchain_id:int8 address:bits256 = MsgAddressInt;
addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9)
workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
_ _:MsgAddressInt = MsgAddress;
_ _:MsgAddressExt = MsgAddress;
//
var_uint$_ {n:#} len:(#< n) value:(uint (len * 8))
= VarUInteger n;
var_int$_ {n:#} len:(#< n) value:(int (len * 8))
= VarInteger n;
nanograms$_ amount:(VarUInteger 16) = Grams;
//
extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32))
= ExtraCurrencyCollection;
currencies$_ grams:Grams other:ExtraCurrencyCollection
= CurrencyCollection;
//
int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddressInt dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfo;
ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
import_fee:Grams = CommonMsgInfo;
ext_out_msg_info$11 src:MsgAddressInt dest:MsgAddressExt
created_lt:uint64 created_at:uint32 = CommonMsgInfo;
int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddress dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;
ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;
tick_tock$_ tick:Bool tock:Bool = TickTock;
_ split_depth:(Maybe (## 5)) special:(Maybe TickTock)
code:(Maybe ^Cell) data:(Maybe ^Cell)
library:(HashmapE 256 SimpleLib) = StateInit;
simple_lib$_ public:Bool root:^Cell = SimpleLib;
// create a message
message$_ {X:Type} info:CommonMsgInfo
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = Message X;

50
lightning/sign.fif Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env -S fift -s
"TonUtil.fif" include
"lib.fif" include
{
."usage: " @' $0 type ." <input> <output> <key> <key-id>" cr
."Signs multisig <input>.boc file with private key <key-id> loaded from file <key>.pk and writes result to <output>.boc" cr 1 halt
} : usage
$# 4 < ' usage if
$1 =: input-file
$2 =: output-file
$3 =: key
$4 parse-int =: key-id
input-file +".boc" load-boc constant order
key +".pk" load-keypair nip constant wallet_pk
order inspect cr
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
message-contents
1 u@+ nip // $0 was already verified
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
dict@+
s>c // Message on top, signatures underneath
dup ."signing message: " <s csr. cr
dup hash wallet_pk ed25519_sign_uint
256 B>u@+ swap 256 B>u@ swap
<b swap 256 u, swap 256 u, b> <s
rot
// Now we have (message) value dict
// udict! => value key dict bits
key-id swap 4 udict! not abort"Failure adding signature!"
// Now we have message dict
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
<b 0 1 u, swap dict, swap <s s, b>
// Renerate external message
<b 68 7 u, wallet-addr addr, 0 Gram, 0 1 u, swap maybe-ref, b>
2 boc+>B dup Bx. cr
output-file +".boc" tuck B>file
."Saved new multisigned message to file " type cr

22
lightning/test-collator.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash -ex
{
address=$(cat tests/naddr.addr)
cd ../lib/build
rm -rf new
mkdir -p new
cd new
mkdir -p db/static
../crypto/create-state ../../../toolchain/gen-zerostate.fif | tee log
mv basestate0.boc db/static/$(sed '/Basestate0 file hash= /!d;s/Basestate0 file hash= //g;s/ .*//g' log)
cp zerostate.boc db/static/$(sed '/Zerostate file hash= /!d;s/Zerostate file hash= //g;s/ .*//g' log)
rm log
fift -s ../../../toolchain/testgiver.fif $address 0 20
../test-ton-collator -z zerostate.boc -D db -v5 -m testgiver-query.boc
../test-ton-collator -z zerostate.boc -D db -v5 -w 0 -m ../../../wallet/tests/pony-create.boc -m ../../../wallet/tests/b.boc -m ../../../wallet/tests/code-update-merge.boc -m ../../../wallet/tests/c.boc
} 2>&1 | less -R

48
lightning/test-update.sh Executable file
View File

@ -0,0 +1,48 @@
#!/bin/bash -e
# Define some helper functions
chr() { [ "$1" -lt 256 ] || return 1; printf "\\$(printf '%03o' "$1")"; }
ord() { LC_CTYPE=C printf '%d' "'$1"; }
cd tests
{
# Sign previous simple wallet query (a, b) with key c (a, b, c)
fift -s ../sign.fif b abc c 2
# Update the wallet using code from wallet-code-update.fif,
# also substituting the 10 keys with only 3 and setting k to 3
fift -s ../wallet-update.fif pony code-update-a 1 3 3 0 a b c
# Sign the query using all keys separately, creating eight more boc files, each signed by two keys only (0 and 1..9)
for f in {1..9}; do fift -s ../sign.fif code-update-a code-update-$(chr $((97+f))) $(chr $((97+f))) $f;done
# Merge all queries
fift -s ../merge.fif code-update-{a..j} code-update-merge
# Inspect queries
fift -s ../inspect.fif merge
# Finally run the generated files in the VM
#
# First init VM with constructor message
# Then load first file with only two signatures by key a, b (0, 1)
# Then load wallet code update (0)
# Run seqno get-method
# Run getPartialsByKeyId get-method
# Load wallet code update with all signatures (and UPDATE CODE)
# Run getPartialsByKeyId get-method
# Finally load file with three signatures by keys a, b, c (0, 1, 2)
#
# The latter will actually send the message, since now only three signatures are required to send a message (but the wallet code update was signed by all 10)
# This will not actually update the code, since there is no way (yet) to get a list of output actions generated by the TON VM from fift,
# however the message **will be sent** since the minSig and keys fields in persistent storage will be updated.
#
fift -s ../test.fif \
pony-create \
b -1 \
code-update-a -1 \
0 85143 \
0 113609 \
code-update-merge -1 \
abc -1
} 2>&1 | less -R

114
lightning/test.fif Normal file
View File

@ -0,0 +1,114 @@
"TonUtil.fif" include
"lib.fif" include
{
."usage: " @' $0 type ." <init-message> <message> <func> [ <message2> <func2> ... ]" cr
."Runs the function with ID <func> in the TON VM, using code and storage data from <init-message>.boc and <message>.boc as message (OR as the only integer parameter of get-method, if <message>.boc does not exist)." cr 1 halt
} : usage
$# 3 < ' usage if
$1 +".boc" load-boc const init-boc
$# 1- 2 /c const message-count
variable-set function function!
variable-set code code!
variable-set storage storage!
variable-set retcode retcode!
// c7
// [ magic:0x076ef1ea actions:Integer msgs_sent:Integer
// unixtime:Integer block_lt:Integer trans_lt:Integer
// rand_seed:Integer balance_remaining:[Integer (Maybe Cell)]
// myself:MsgAddressInt global_config:(Maybe Cell)
// ] = SmartContractInfo;
0x076ef1ea 0 0 now 0 0 <b 1234 32 u, b> hash 7 tuple 1 tuple const ctx
// message$_ {X:Type} info:CommonMsgInfo
// init:(Maybe (Either StateInit ^StateInit))
// body:(Either X ^X) = Message X;
init-boc <s
// External message
7 u@+ swap 68 <> { ."There seems to be an invalid header" cr } if // 1000100 => 68
8 i@+
256 u@+ -rot
."Init wallet address: " .addr cr
Gram@+ nip // Ignore grams
1 u@+ swap
1- abort"This does not seem to be an init message"
1 u@+ swap
{ ."Loading ref..." cr ref@ <s } if // Load ref to StateInit if ref
// _ split_depth:(Maybe (## 5)) special:(Maybe TickTock)
// code:(Maybe ^Cell) data:(Maybe ^Cell)
// library:(HashmapE 256 SimpleLib) = StateInit;
1 u@+ swap
{ ."Has split_depth" cr 5 u@+ nip } if // Ignore split_depth
1 u@+ swap
{ ."Has TikTok" cr 2 u@+ nip } if // Ignore TikTok ;)
1 u@+ swap
1- abort"No code!"
ref@+ <s code! // Load code
1 u@+ swap
1- abort"No data!"
ref@+ storage! // Load storage
1 u@+ nip // Skip libs
1 u@+ swap
{ ."Loading body ref..." cr ref@ } if // Load ref to body if ref
message-contents! // Load init message
-1 function! // External message
{
."INIT: Calling " function . cr cr
message-contents function code storage ctx runvmctx .s
swap retcode!
."Retcode: " retcode . cr
dup storage!
inspect-storage
retcode 0 <> abort"Exception"
} : ponyvm
cr ."===================================================" cr cr
ponyvm
2 { dup 1+ swap // Increase counter
cr ."===================================================" cr cr
$() +".boc"
dup file-exists?
{
load-boc inspect // Automatically sets message-contents
}
{
drop
dup 1- $() parse-int
// <b swap 256 u, b>
message-contents!
} cond
dup 1+ swap // Increase counter
$() parse-int function!
ponyvm
// Manually drop return values of functions
function -1 <> {
2drop
} if
// rot
// ."Signature: "
// 64 B@ Bx.
} message-count times

57
lightning/test.sh Executable file
View File

@ -0,0 +1,57 @@
#!/bin/bash -e
# Define some helper functions
chr() { [ "$1" -lt 256 ] || return 1; printf "\\$(printf '%03o' "$1")"; }
ord() { LC_CTYPE=C printf '%d' "'$1"; }
{
mkdir -p tests
cd tests
# Create 10 public keys
for f in {a..j}; do fift -s ../gen-pub.fif $f;done
# Create wallet with those 10 public keys on workchain 0, requiring all 10 signatures to send a message
fift -s ../wallet-create.fif 0 pony 10 10 {a..j} | tee log
# Get wallet address
sed '/Non-bounceable address [(]for init[)]: /!d;s/.* //g' log > naddr.addr
address=$(sed '/Bounceable address [(]for later access[)]: /!d;s/.* //g' log)
rm log
# Create a new wallet query signed with key a (ID 0), transferring 10 grams to the wallet itself
fift -s ../create.fif pony a 0 $address 0 10 a
# Here, note the `a 0`: to save space, I have chosen to not use the entire ecdh key as key in the signature dictionary.
# Instead, a **key ID** is used to distinguish signatures made by certain keys: this is a simple 4-bit value (instead of 256 bits!), equal to the position of the key in the `wallet-create` argument list.
# In this case, the `a` key was the first key (`{a..j}` in Bash is shorthand for `a b c .. j`), so the ID is `0`.
# What follows is the address, the seqno, the amount of grams and the savefile (`a`) for the query.
# Sign the query using all keys separately, creating eight more boc files, each signed by two keys only (0 and 1..9)
for f in {1..9}; do fift -s ../sign.fif a $(chr $((97+f))) $(chr $((97+f))) $f;done
# Merge all queries
fift -s ../merge.fif {a..j} merge
# Inspect queries
fift -s ../inspect.fif merge
# Finally run the generated files in the VM
#
# First init VM with constructor message
# Then load first file with only one signature by key a (0)
# Run seqno get-method
# Run getPartialsByKeyId get-method
# Load file with all signatures (and send message)
# Run getPartialsByKeyId get-method
#
fift -s ../test.fif \
pony-create \
a -1 \
0 85143 \
0 113609 \
merge -1 \
0 113609
} 2>&1 | less -R

41
lightning/verify.fif Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env -S fift -s
"TonUtil.fif" include
"lib.fif" include
{
."usage: " @' $0 type ." <input> <key1> <key1-id> [<key2> <key2-id> ...]" cr
."Verify multisig <input>.boc file with public key <keyN-id> loaded from file <keyN>.pubkey" cr 1 halt
} : usage
$# 3 < ' usage if
$# 3 - 2 mod ' usage if
$1 +".boc" load-boc constant order
order inspect cr
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
message-contents
1 u@+ nip // $0 was already verified
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
dict@ const signatures
variable-set key key!
variable-set key-id key-id!
2 { dup 1+ swap // Counter
$() +".pubkey" load-pubkey key!
dup 1+ swap // Increment counter
$() parse-int key-id!
."Checking signature of key ID " key-id . ."... "
key-id signatures 4 dict[] // Find signature
not abort"Could not find signature with specified key ID!"
64 B@
// udata BSig ukey ?
message-hash swap key
ed25519_chksignuu
not abort"Invalid signature!"
."OK!" cr
} $# 1- 2 /c times

View File

@ -0,0 +1,236 @@
;; Multisig wallet smart contract (Daniil Gentili @danogentili <daniil@daniil.it>)
;; (Now unused) tuple manipulation primitives for integers
tuple tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
(tuple, ()) ~tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
;; Tuple manipulation primitives for cells
tuple tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
(tuple, ()) ~tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
int tuple_len(tuple t) asm "TLEN";
cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
int udict_has?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT" "NIP";
;; Cleanup expired partial orders
;; messages
(cell, ()) ~collect_garbage(cell messages) {
var hash = -1;
do {
(hash, var cs, int ok) = messages.udict_get_next?(256, hash);
if (ok) {
;; modeMessage$0 mode:uint8 body:^(Message X) = ModeMessage X;
;; wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X;
;; multiSigWrapperStorage$_ count:(## 4) signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapperStorage X;
;;
;; Skip signatures, check expiry
;;
;; expiry <= now
if (cs.skip_bits(4).skip_dict().preload_uint(32) <= now()) {
messages~udict_delete?(256, hash);
}
}
} until (~ ok);
return (messages, ());
}
() store_db(int seqno, cell keys, cell messages) {
set_data(begin_cell().store_uint(seqno, 32).store_dict(keys).store_dict(messages).end_cell());
}
() recv_internal(slice message) impure {
;; do nothing for internal messages
}
;; multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
() recv_external(slice message) impure {
;; Check if multiSigWrapper$0, multiSigInit$10 or future unsupported protocol
int tag = message~load_uint(1);
if (tag) {
;; Throw if multiSigFuture$11
throw_if(32, message~load_uint(1));
;; Accept if multiSigInit$10
accept_message();
slice stored_data = get_data().begin_parse();
(int stored_seqno, int min_sigs, var keys, var messages) = (stored_data~load_uint(32), stored_data~load_uint(4), stored_data~load_dict(), stored_data~load_dict());
stored_data.end_parse();
set_data(begin_cell().store_uint(stored_seqno, 32).store_uint(min_sigs, 4).store_dict(keys).store_dict(messages).end_cell());
return ();
}
;; Check if is hme_empty$0 or hme_root$1
;; Throw if empty signature list
throw_unless(33, message.preload_uint(1));
var signatures = message~load_dict();
slice message_copy = message;
;; wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X;
(int expires_at, int msg_seqno) = (message_copy~load_uint(32), message_copy~load_uint(32));
;; Message expired
throw_if(34, expires_at <= now());
;; We will need the hash anyway
int hash = slice_hash(message);
;; storage$_ seqno:uint32 minSigs:(## 4) keys:(HashmapE 4 PubKey) messages:(HashmapE 256 (MultiSigWrapperStorage X)) = Storage X;
slice stored_data = get_data().begin_parse();
(int stored_seqno, int min_sigs, var keys, var messages) = (stored_data~load_uint(32), stored_data~load_uint(4), stored_data~load_dict(), stored_data~load_dict());
stored_data.end_parse();
;; This is a new message, so there will be no stored message
var storedSignatureCount = 0;
var storedMessageSignatures = new_dict();
;; If new message, check and increase seqno
if (stored_seqno == msg_seqno) {
stored_seqno += 1;
;; If old message, seqno WILL be different, check if exists in unsigned messages dict
} else {
(message, int ok) = messages.udict_get?(256, hash);
;; Throw if old message and doesn't exist in db
throw_unless(35, ok);
;; multiSigWrapperStorage$_ count:(## 4) signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapperStorage X;
;;
;; Load signatures
storedSignatureCount = message~load_uint(4);
storedMessageSignatures = message.preload_dict();
}
accept_message();
int idx = -1;
do {
(idx, slice signature, int ok) = signatures.udict_get_next?(4, idx);
if (ok) {
(var public_key, ok) = keys.udict_get?(4, idx);
throw_unless(36, ok);
var slice_copy = signature;
throw_unless(37, check_signature(hash, slice_copy, public_key.preload_uint(256)));
if (~ storedMessageSignatures.udict_has?(4, idx)) {
storedMessageSignatures~udict_set(4, idx, signature);
storedSignatureCount += 1;
}
}
} until (~ ok);
if (storedSignatureCount >= min_sigs) {
if (message_copy~load_uint(1)) {
;; Code upgrade
;; codeMessage$1 minSigs:(## 4) keys:(HashmapE 4 PubKey) code:^Cell = ModeMessage X;
;;
min_sigs = message_copy~load_uint(4);
keys = message_copy~load_dict();
set_code(message_copy~load_ref());
} else {
;; Simple message
;; modeMessage$0 mode:uint8 body:^(Message X) = ModeMessage X;
;;
var (mode, message) = (message_copy~load_uint(8), message_copy~load_ref());
send_raw_message(message, mode);
}
message_copy.end_parse();
messages~udict_delete?(256, hash);
} else {
;; modeMessage$0 mode:uint8 body:^(Message X) = ModeMessage X;
;; wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X;
;; multiSigWrapperStorage$_ count:(## 4) signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapperStorage X;
;;
messages~udict_set_builder(
256,
hash,
begin_cell()
.store_uint(storedSignatureCount, 4)
.store_dict(storedMessageSignatures)
.store_uint(expires_at, 32)
.store_uint(msg_seqno, 32)
.store_slice(message_copy)
);
}
messages~collect_garbage();
;; storage$_ seqno:uint32 minSigs:(## 4) keys:(HashmapE 4 PubKey) messages:(HashmapE 256 (MultiSigWrapperStorage X)) = Storage X;
set_data(begin_cell().store_uint(stored_seqno, 32).store_uint(min_sigs, 4).store_dict(keys).store_dict(messages).end_cell());
}
;; Get methods
int seqno() method_id {
return get_data().begin_parse().preload_uint(32);
}
(int, int) getKey(int id) method_id {
(var res, int ok) = get_data().begin_parse().skip_bits(36).preload_dict().udict_get?(4, id);
if (~ ok) {
return (ok, 0);
}
return (-1, res.preload_uint(256));
}
(int, int) getId(int key) method_id {
var keys = get_data().begin_parse().skip_bits(36).preload_dict();
int idx = -1;
int ok = 0;
int found = 0;
do {
(idx, var keyRead, ok) = keys.udict_get_next?(4, idx);
if (ok) {
;; I could actually return here, but the funcompiler won't let me
found = keyRead.preload_uint(256) == key;
}
} until ((~ ok) | found);
if (found) {
return (-1, idx);
}
return (0, 0);
}
(cell) getPartials() method_id {
return get_data().begin_parse().skip_bits(36).skip_dict().preload_dict();
}
(int, cell) getPartialsByKeyId(int id) method_id {
cell messages = get_data().begin_parse().skip_bits(36).skip_dict().preload_dict();
cell messages_found = new_dict();
int idx = -1;
int ok = 0;
int found = 0;
do {
(idx, slice message, ok) = messages.udict_get_next?(256, idx);
if (ok) {
if (message.skip_bits(4).preload_dict().udict_has?(4, id)) {
found += 1;
messages_found~udict_set_builder(256, idx, begin_cell().store_slice(message));
}
}
} until (~ ok);
return (found, messages_found);
}
(int, cell) getPartialByKey(int key) method_id {
(int ok, int id) = getId(key);
ifnot (ok) {
return (0, new_dict());
}
return getPartialsByKeyId(id);
}
(int, cell) getPartialByHash(int hash) method_id {
(slice message, int ok) = get_data().begin_parse().skip_bits(36).skip_dict().preload_dict().udict_get?(256, hash);
ifnot (ok) {
return (0, begin_cell().end_cell());
}
return (ok, begin_cell().store_slice(message).end_cell());
}
(int, int) getMagic() method_id {
return (420, 69);
}

View File

@ -0,0 +1,397 @@
"Asm.fif" include
// automatically generated from `/home/daniil/repos/contest/lib/crypto/smartcont/stdlib.fc` `wallet-code-update.fc`
PROGRAM{
DECLPROC ~collect_garbage
DECLPROC store_db
DECLPROC recv_internal
DECLPROC recv_external
85143 DECLMETHOD seqno
119659 DECLMETHOD getKey
66593 DECLMETHOD getId
124239 DECLMETHOD getPartials
113609 DECLMETHOD getPartialsByKeyId
125107 DECLMETHOD getPartialByKey
100959 DECLMETHOD getPartialByHash
77784 DECLMETHOD getMagic
~collect_garbage PROC:<{
-1 PUSHINT
UNTIL:<{
OVER
8 PUSHPOW2
DICTUGETNEXT
NULLSWAPIFNOT
NULLSWAPIFNOT
DUP
IF:<{
s0 s2 XCHG
4 PUSHINT
SDSKIPFIRST
SKIPDICT
32 PLDU
NOW
LEQ
IF:<{
8 PUSHPOW2
s1 s3 s3 PUXC2
DICTUDEL
DROP
}>ELSE<{
s0 s2 XCHG
}>
}>ELSE<{
2SWAP
DROP
}>
SWAP
NOT
s1 s2 XCHG
}>
DROP
}>
store_db PROC:<{
NEWC
s1 s3 XCHG
32 STU
STDICT
STDICT
ENDC
c4 POP
}>
recv_internal PROC:<{
DROP
}>
recv_external PROC:<{
1 LDU
SWAP
IFJMP:<{
1 LDU
DROP
32 THROWIF
ACCEPT
c4 PUSH
CTOS
32 LDU
4 LDU
LDDICT
LDDICT
ENDS
NEWC
s1 s4 XCHG
32 STU
s1 s2 XCHG
4 STU
STDICT
STDICT
ENDC
c4 POP
}>
DUP
1 PLDU
33 THROWIFNOT
LDDICT
DUP
32 LDU
32 LDU
s2 PUSH
NOW
LEQ
34 THROWIF
s0 s3 XCHG
HASHSU
c4 PUSH
CTOS
32 LDU
4 LDU
LDDICT
LDDICT
ENDS
0 PUSHINT
NEWDICT
s5 s7 PUSH2
EQUAL
IF:<{
s0 s5 XCHG
INC
}>ELSE<{
2DROP
s4 s0 PUSH2
8 PUSHPOW2
DICTUGET
NULLSWAPIFNOT
35 THROWIFNOT
4 LDU
PLDDICT
s0 s5 XCHG
}>
ACCEPT
-1 PUSHINT
UNTIL:<{
s11 PUSH
4 PUSHINT
DICTUGETNEXT
NULLSWAPIFNOT
NULLSWAPIFNOT
DUP
IF:<{
DROP
s0 s5 PUSH2
4 PUSHINT
DICTUGET
NULLSWAPIFNOT
DUP
36 THROWIFNOT
s3 s(-1) s0 PUXC2
256 PLDU
s11 s2 s2 PUXC2
CHKSIGNU
37 THROWIFNOT
s1 s8 PUSH2
4 PUSHINT
DICTUGET
NULLSWAPIFNOT
NIP
NOT
IF:<{
s1 s2 XCHG
4 PUSHINT
s3 s9 s9 PUXC2
DICTUSET
s0 s3 XCHG
INC
}>ELSE<{
s0 s8 XCHG
s1 s4 s4 XCHG3
DROP
}>
}>ELSE<{
s0 s8 XCHG
s1 s4 s4 XCHG3
DROP
}>
s0 s7 XCHG
NOT
s3 s7 XCHG
}>
DROP
s10 POP
s0 s3 PUSH2
GEQ
IF:<{
DROP
s3 POP
s4 POP
s4 POP
s0 s4 XCHG
1 LDU
SWAP
IF:<{
s2 POP
s2 POP
4 LDU
LDDICT
LDREF
SWAP
SETCODE
}>ELSE<{
8 LDU
LDREF
s0 s2 XCHG
SENDRAWMSG
s3 s3 s0 XCHG3
}>
ENDS
s2 s3 XCHG2
8 PUSHPOW2
DICTUDEL
DROP
}>ELSE<{
NEWC
4 STU
s1 s4 XCHG
STDICT
s1 s6 XCHG
32 STU
s1 s4 XCHG
32 STU
s0 s5 XCHG2
STSLICER
s0 s1 s4 XCHG3
8 PUSHPOW2
DICTUSETB
s1 s2 XCHG
}>
~collect_garbage CALLDICT
NEWC
s1 s4 XCHG
32 STU
4 STU
STDICT
STDICT
ENDC
c4 POP
}>
seqno PROC:<{
c4 PUSH
CTOS
32 PLDU
}>
getKey PROC:<{
c4 PUSH
CTOS
36 PUSHINT
SDSKIPFIRST
PLDDICT
4 PUSHINT
DICTUGET
NULLSWAPIFNOT
DUP
NOT
IFJMP:<{
NIP
0 PUSHINT
}>
DROP
-1 PUSHINT
SWAP
256 PLDU
}>
getId PROC:<{
c4 PUSH
CTOS
36 PUSHINT
SDSKIPFIRST
PLDDICT
-1 PUSHINT
0 PUSHINT
UNTIL:<{
s1 s2 XCPU
4 PUSHINT
DICTUGETNEXT
NULLSWAPIFNOT
NULLSWAPIFNOT
DUP
IF:<{
s3 POP
SWAP
256 PLDU
s4 PUSH
EQUAL
}>ELSE<{
s1 s3 s3 XCHG3
DROP
}>
s0 s2 XCHG
NOT
s2 PUSH
OR
s1 s2 XCHG
}>
s2 POP
s2 POP
IFJMP:<{
-1 PUSHINT
SWAP
}>
DROP
0 PUSHINT
DUP
}>
getPartials PROC:<{
c4 PUSH
CTOS
36 PUSHINT
SDSKIPFIRST
SKIPDICT
PLDDICT
}>
getPartialsByKeyId PROC:<{
c4 PUSH
CTOS
36 PUSHINT
SDSKIPFIRST
SKIPDICT
PLDDICT
NEWDICT
-1 PUSHINT
0 PUSHINT
UNTIL:<{
s1 s3 XCPU
8 PUSHPOW2
DICTUGETNEXT
NULLSWAPIFNOT
NULLSWAPIFNOT
DUP
IF:<{
s2 PUSH
4 PUSHINT
SDSKIPFIRST
PLDDICT
s7 s(-1) PUXC
4 PUSHINT
DICTUGET
NULLSWAPIFNOT
NIP
IF:<{
s0 s3 XCHG
INC
NEWC
s0 s3 XCHG2
STSLICER
SWAP
8 PUSHPOW2
s1 s5 s5 PUXC2
DICTUSETB
}>ELSE<{
s3 s4 s3 XCHG3
DROP
}>
}>ELSE<{
s3 s4 s3 XCHG3
DROP
}>
s0 s2 XCHG
NOT
s2 s3 XCHG
}>
NIP
s2 POP
s2 POP
SWAP
}>
getPartialByKey PROC:<{
getId CALLDICT
SWAP
IFNOTJMP:<{
DROP
0 PUSHINT
NEWDICT
}>
getPartialsByKeyId CALLDICT
}>
getPartialByHash PROC:<{
c4 PUSH
CTOS
36 PUSHINT
SDSKIPFIRST
SKIPDICT
PLDDICT
8 PUSHPOW2
DICTUGET
NULLSWAPIFNOT
DUP
IFNOTJMP:<{
2DROP
0 PUSHINT
NEWC
ENDC
}>
NEWC
ROT
STSLICER
ENDC
}>
getMagic PROC:<{
420 PUSHINT
69 PUSHINT
}>
}END>c

217
lightning/wallet-code.fc Normal file
View File

@ -0,0 +1,217 @@
;; Multisig wallet smart contract (Daniil Gentili @danogentili <daniil@daniil.it>)
cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
int udict_has?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT" "NIP";
slice skip_grams(slice s) asm "LDGRAMS" "NIP";
int preload_grams(slice s) asm "LDGRAMS" "DROP";
;; twoSignatures$_ a:^Signature b:^Signature = MaybeTwoSignatures;
;; oneSignatureA$0 a:^Signature = MaybeTwoSignatures;
;; oneSignatureB$1 b:^Signature = MaybeTwoSignatures;
;;
slice skip_sigs(slice data) {
if (data.slice_refs() == 2) {
return data;
} else {
return data.skip_bits(1);
}
}
;; twoSignatures$_ a:^Signature b:^Signature = MaybeTwoSignatures;
;; oneSignatureA$0 a:^Signature = MaybeTwoSignatures;
;; oneSignatureB$1 b:^Signature = MaybeTwoSignatures;
;;
;; sigMask: 3 (11) => AB, 2 (10) => B, 1 (01) => A
;;
;; int sigMask, cell sigA, cell sigB
(slice, (int, cell, cell)) ~load_sigs(slice data) {
if (data.slice_refs() == 2) { ;; Two signatures
return (data, (3, data~load_ref(), data~load_ref()));
} else {
if (data~load_uint(1)) { ;; One signature (B)
return (data, (2, new_dict(), data~load_ref()));
} else { ;; One signature (A)
return (data, (1, data~load_ref(), new_dict()));
}
}
}
(int, cell, cell) preload_sigs(slice data) {
return data~load_sigs();
}
;; twoSignatures$_ a:^Signature b:^Signature = MaybeTwoSignatures;
;; oneSignatureA$0 a:^Signature = MaybeTwoSignatures;
;; oneSignatureB$1 b:^Signature = MaybeTwoSignatures;
;;
;; sigMask: 3 (11) => AB, 2 (10) => B, 1 (01) => A
;;
builder store_sigs(builder b, int sigMask, cell sigA, cell sigB) {
if (sigMask == 3) {
return b.store_ref(sigA).store_ref(sigB);
} else {
if (sigMask == 2) {
return b.store_uint(1, 1).store_ref(sigB);
} else { ;; Assume 1, since 0 is not possible
return b.store_uint(0, 1).store_ref(sigA);
}
}
}
;;
;; state$_ signatures:MaybeTwoSignatures seqNo:uint32 delta:Grams final:Bool {turnB:(seqNo % 2)} = State;
;; storage$_ keyA:PubKey keyB:PubKey amountA:Grams amountB:Grams state:State = Storage X;
;;
;; int keyA, int keyB, int amountA, int amountB, int sigMask, cell sigA, cell sigB, int seqNo, int delta, int final
(int, int, int, int, int, cell, cell, int, int, int) load_storage() {
slice stored_data = get_data().begin_parse();
(int keyA, int keyB, int amountA, int amountB, (int sigMask, cell sigA, cell sigB), int seqNo, int delta, int final) = (
stored_data~load_uint(256),
stored_data~load_uint(256),
stored_data~load_grams(),
stored_data~load_grams(),
stored_data~load_sigs(),
stored_data~load_uint(32),
stored_data~load_grams(),
stored_data~load_uint(1));
stored_data.end_parse();
return (keyA, keyB, amountA, amountB, sigMask, sigA, sigB, seqNo, delta, final);
}
() store_storage(int keyA, int keyB, int amountA, int amountB, int sigMask, cell sigA, cell sigB, int seqNo, int delta, int final) {
set_data(
begin_cell()
.store_uint(keyA, 256)
.store_uint(keyB, 256)
.store_grams(amountA)
.store_grams(amountB)
.store_sigs(sigMask, sigA, sigB)
.store_uint(seqNo, 32)
.store_grams(delta)
.store_uint(final, 1)
.end_cell());
return ();
}
;; Cleanup expired partial orders
() recv_internal(slice message) impure {
;; do nothing for internal messages
}
;; incomingState$_ signatures:MaybeTwoSignatures seqNo:uint32 delta:Grams final:Bool {turnB:(seqNo % 2)} = Incoming;
;;
() recv_external(slice message) impure {
(int msgSigMask, cell msgSigA, cell msgSigB) = message~load_sigs();
slice message_copy = message;
(int msgSeqNo, int msgDelta, int msgFinal) = (message_copy~load_uint(32), message~load_grams(), message~load_uint(1));
(int keyA, int keyB, int amountA, int amountB, int sigMask, cell sigA, cell sigB, int seqNo, int delta, int final) = load_storage();
int diff = msgSeqNo - seqNo;
;; Missing some inbetween state
throw_if(33, diff > 1);
;; Old message
throw_if(34, diff < 0);
if (diff) { ;; > 0, New state
;; If current state isn't complete yet
throw_unless(35, sigMask == 3);
;; Apply new seqNo and reset sigMask
seqNo = msgSeqNo;
sigMask = 0;
;; Make sure the message delta is correct for the current turn
;;
;; A and B own a+δ and b δ coins, respectively
;;
;; Thus, A can only make δ smaller (not bigger!)
;; Thus, B can only make δ bigger (not smaller!)
;;
int deltaDiff = msgDelta - delta;
if (seqNo % 2) { ;; If it's B's turn
;; B can only make δ bigger (not smaller!)
throw_if(36, deltaDiff < 0);
;; You can't transfer more than amountB funds to A
throw_if(37, amountB - msgDelta < 0);
} else { ;; If it's A's turn
;; A can only make δ smaller (not bigger!)
throw_if(36, deltaDiff > 0);
;; You can't transfer more than amountA funds to B
throw_if(37, amountA + msgDelta < 0);
}
delta = msgDelta;
final = msgFinal;
} else { ;; == 0, Completion of state
;; If current state is already complete
throw_if(35, sigMask == 3);
;; Make sure message is the same (equivalent to checking the hash)
throw_unless(38, msgDelta == delta);
throw_unless(39, msgFinal == final);
}
accept_message();
int hash = slice_hash(message);
;; Only check missing signatures, do not recheck existing ones
msgSigMask &= ~ sigMask;
if (msgSigMask & 1) { ;; Check A
throw_unless(39, check_signature(hash, msgSigA.begin_parse().preload_bits(512), keyA));
sigA = msgSigA;
sigMask |= 1;
}
if (msgSigMask & 2) { ;; Check B
throw_unless(40, check_signature(hash, msgSigB.begin_parse().preload_bits(512), keyB));
sigB = msgSigB;
sigMask |= 2;
}
if (sigMask == 3 & final) { ;; Close stuffs
}
;; int keyA, int keyB, int amountA, int amountB, int sigMask, cell sigA, cell sigB, int seqNo, int delta, int final
store_storage(keyA, keyB, amountA, amountB, sigMask, sigA, sigB, seqNo, delta, final);
}
;; Get methods
;;
;; state$_ signatures:MaybeTwoSignatures seqNo:uint32 delta:Grams final:Bool {turnB:(seqNo % 2)} = State;
;; storage$_ keyA:PubKey keyB:PubKey amountA:Grams amountB:Grams state:State = Storage X;
int seqNo() method_id {
return get_data().begin_parse().skip_bits(512).skip_grams().skip_grams().skip_sigs().preload_uint(32);
}
int getDelta() method_id {
return get_data().begin_parse().skip_bits(512).skip_grams().skip_grams().skip_sigs().skip_bits(32).preload_grams();
}
int isTurnA() method_id {
return ~(seqNo() % 2);
}
int isTurnB() method_id {
return seqNo() % 2;
}
int getAmountA() method_id {
return get_data().begin_parse().skip_bits(512).preload_grams();
}
int getAmountB() method_id {
return get_data().begin_parse().skip_bits(512).skip_grams().preload_grams();
}
(int, cell, cell) getSignatures() method_id {
return get_data().begin_parse().skip_bits(512).skip_grams().skip_grams().preload_sigs();
}
int getHash() method_id {
return slice_hash(get_data().begin_parse().skip_bits(512).skip_grams().skip_grams().skip_sigs());
}

330
lightning/wallet-code.fif Normal file
View File

@ -0,0 +1,330 @@
"Asm.fif" include
// automatically generated from `/home/daniil/repos/contest/lib/crypto/smartcont/stdlib.fc` `wallet-code.fc`
PROGRAM{
DECLPROC skip_sigs
DECLPROC ~load_sigs
DECLPROC preload_sigs
DECLPROC store_sigs
DECLPROC load_storage
DECLPROC store_storage
DECLPROC recv_internal
DECLPROC recv_external
84593 DECLMETHOD seqNo
125129 DECLMETHOD getDelta
112390 DECLMETHOD isTurnA
100197 DECLMETHOD isTurnB
101812 DECLMETHOD getAmountA
114135 DECLMETHOD getAmountB
78055 DECLMETHOD getSignatures
120167 DECLMETHOD getHash
skip_sigs PROC:<{
DUP
SREFS
2 EQINT
IFJMP:<{
}>
1 PUSHINT
SDSKIPFIRST
}>
~load_sigs PROC:<{
DUP
SREFS
2 EQINT
IFJMP:<{
3 PUSHINT
SWAP
LDREF
LDREF
3 -ROLL
}>
1 LDU
SWAP
IFJMP:<{
2 PUSHINT
NEWDICT
s0 s2 XCHG
LDREF
s3 s3 XCHG2
}>
1 PUSHINT
SWAP
LDREF
NEWDICT
s3 s3 s0 XCHG3
}>
preload_sigs PROC:<{
~load_sigs CALLDICT
s3 POP
ROT
}>
store_sigs PROC:<{
s2 PUSH
3 EQINT
IFJMP:<{
s2 POP
ROT
STREF
STREF
}>
s0 s2 XCHG
2 EQINT
IFJMP:<{
DROP
1 PUSHINT
ROT
1 STU
STREF
}>
NIP
0 PUSHINT
ROT
1 STU
STREF
}>
load_storage PROC:<{
c4 PUSH
CTOS
256 LDU
256 LDU
LDGRAMS
LDGRAMS
~load_sigs CALLDICT
s0 s3 XCHG
32 LDU
LDGRAMS
1 LDU
ENDS
s4 s5 XCHG
s3 s4 XCHG
}>
store_storage PROC:<{
NEWC
s1 s10 XCHG
256 STU
s1 s8 XCHG
256 STU
s0 s6 XCHG2
STGRAMS
s0 s4 XCHG2
STGRAMS
s0 s3 XCHG
store_sigs CALLDICT
32 STU
SWAP
STGRAMS
1 STU
ENDC
c4 POP
}>
recv_internal PROC:<{
DROP
}>
recv_external PROC:<{
~load_sigs CALLDICT
s3 PUSH
32 LDU
DROP
s0 s4 XCHG
LDGRAMS
1 LDU
load_storage CALLDICT
s3 POP
s3 POP
s14 s0 PUSH2
SUB
NIP
DUP
1 GTINT
33 THROWIF
DUP
0 LESSINT
34 THROWIF
IF:<{
DROP
SWAP
3 EQINT
35 THROWIFNOT
s7 PUSH
0 PUSHINT
s0 s2 XCHG
SUB
s12 PUSH
1 MODPOW2#
s13 POP
s0 s12 XCHG
IF:<{
s2 POP
s0 s10 XCHG
0 LESSINT
36 THROWIF
s9 s5 PUSH2
SUB
s6 POP
s9 POP
s0 s4 XCHG
0 LESSINT
37 THROWIF
}>ELSE<{
NIP
s0 s10 XCHG
0 GTINT
36 THROWIF
s0 s5 PUSH2
ADD
NIP
s5 POP
s0 s4 XCHG
0 LESSINT
37 THROWIF
}>
}>ELSE<{
s3 POP
s3 POP
s11 POP
s10 PUSH
3 EQINT
35 THROWIF
s6 s1 XCPU
EQUAL
NIP
38 THROWIFNOT
s3 s4 XCPU
EQUAL
39 THROWIFNOT
s2 s3 XCHG
}>
ACCEPT
SWAP
HASHSU
s7 PUSH
NOT
s1 s7 XCHG
AND
DUP
1 PUSHINT
AND
IF:<{
s5 PUSH
CTOS
s6 POP
s0 s5 XCHG
9 PUSHPOW2
PLDSLICEX
s6 s1 s(-1) PU2XC
CHKSIGNU
NIP
39 THROWIFNOT
s0 s5 XCHG
1 PUSHINT
OR
}>ELSE<{
s7 s1 s5 XCHG3
2DROP
}>
s0 s3 XCHG
2 PUSHINT
AND
IF:<{
OVER
CTOS
s2 POP
SWAP
9 PUSHPOW2
PLDSLICEX
s3 s0 s3 XC2PU
CHKSIGNU
s2 POP
SWAP
40 THROWIFNOT
2 PUSHINT
OR
}>ELSE<{
s2 s3 XCHG
3 BLKDROP
}>
3 PUSHINT
s2 PUSH
AND
s2 POP
s0 s1 PUXC
EQUAL
2DROP
}>
seqNo PROC:<{
c4 PUSH
CTOS
9 PUSHPOW2
SDSKIPFIRST
LDGRAMS
NIP
LDGRAMS
NIP
skip_sigs CALLDICT
32 PLDU
}>
getDelta PROC:<{
c4 PUSH
CTOS
9 PUSHPOW2
SDSKIPFIRST
LDGRAMS
NIP
LDGRAMS
NIP
skip_sigs CALLDICT
32 PUSHINT
SDSKIPFIRST
LDGRAMS
DROP
}>
isTurnA PROC:<{
seqNo CALLDICT
1 MODPOW2#
NOT
}>
isTurnB PROC:<{
seqNo CALLDICT
1 MODPOW2#
}>
getAmountA PROC:<{
c4 PUSH
CTOS
9 PUSHPOW2
SDSKIPFIRST
LDGRAMS
DROP
}>
getAmountB PROC:<{
c4 PUSH
CTOS
9 PUSHPOW2
SDSKIPFIRST
LDGRAMS
NIP
LDGRAMS
DROP
}>
getSignatures PROC:<{
c4 PUSH
CTOS
9 PUSHPOW2
SDSKIPFIRST
LDGRAMS
NIP
LDGRAMS
NIP
preload_sigs CALLDICT
}>
getHash PROC:<{
c4 PUSH
CTOS
9 PUSHPOW2
SDSKIPFIRST
LDGRAMS
NIP
LDGRAMS
NIP
skip_sigs CALLDICT
HASHSU
}>
}END>c

100
lightning/wallet-create.fif Normal file
View File

@ -0,0 +1,100 @@
"TonUtil.fif" include
"lib.fif" include
{ ."usage: " @' $0 type ." <workchain-id> <wallet-name> <n> <k> <privkey1> [<pubkey2> ...]" cr cr
."Creates a new multisignature wallet in specified workchain composed of <n> (1-10) keys." cr
."The first of the keys must be a private key (pre-existing or not), used to generate the wallet; the rest MUST be public keys." cr
."Create or generate public key files from private keys using gen-pub.fif privkey" cr cr
."Min <k> (1-10) signatures required to send an order; load <n> pre-existing public keys from files <key1...n>." cr
1 halt
} : usage
$# 5 < ' usage if
$1 parse-workchain-id =: wc // set workchain id from command line argument
$2 constant file-base
$3 (number) 1 <> abort"<n> must be a number!" constant n
$4 (number) 1 <> abort"<k> must be a number!" constant k
n 1 < n 10 > or abort"<n> must be between 1 and 10"
k 1 < k 10 > or abort"<k> must be between 1 and 10"
k n <= not abort"<k> must smaller than or equal to <n>"
$# 4 n + < abort"Not enough keys were provided in args!"
$5 +".pk" load-generate-keypair const privkey 256 B>u@
6 { dup $() +".pubkey" load-pubkey swap 1+ } n 1- times drop
n tuple constant keys
cr
."Creating new multisignature wallet in workchain " wc .
."with n=" n .
."k=" k . ."..." cr cr
// idict! (v x s n s0 1 or s 0), adds a new value v (represented
// by a Slice) with key given by signed big-endian n-bit integer x into
// dictionary s with n-bit keys, and returns the new dictionary s0 and 1
// on success. Otherwise the unchanged dictionary s and 0 are returned.
// Create dictionaries with keys and messages
// Extract keys
keys explode
dup 1- // Create counter
dictnew swap // ...and dict (swap the two)
rot // Put length on top for times
{ dup 1- swap // Decrement counter (val dict ncount curcount)
3 roll // Get n-th value v (dict ncount curcount val)
<b swap 256 u, // Create builder bval
swap // Get x (dict ncount bval curcount)
3 roll // Get dictionary s (ncount bval curcount dict)
4 // Get n (ncount bval curcount dict 4)
b>udict! // Store (ncount dict')
not abort"Failure storing dictionary value!"
swap // Swap dict (dict' ncount)
} swap times drop const keys-dict
.s
// code
"wallet-code.fif" include
// data
// storage$_ seqno:uint32 minSigs:(## 4) keys:(HashmapE 4 PubKey) messages:(HashmapE 256 (MultiSigWrapperStorage X))
// { minSigs > 0 } { n >= minSigs } { n <= 10 } { minSigs <= 10 } = Storage X;
<b 0 32 u,
k 4 u,
keys-dict dict,
dictnew dict,
b>
// create StateInit
// _ split_depth:(Maybe (## 5)) special:(Maybe TickTock)
// code:(Maybe ^Cell) data:(Maybe ^Cell)
// library:(HashmapE 256 SimpleLib) = StateInit;
// split_depth 0 special 0 code 1 data 1 library hme_empty 0
//
<b b{00110} s, rot ref, swap ref, b>
dup ."StateInit: " <s csr. cr
dup hash wc swap 2dup 2constant wallet-addr
."new wallet address = " 2dup .addr cr
2dup file-base +".addr" save-address-verbose
."Non-bounceable address (for init): " 2dup 7 .Addr cr
."Bounceable address (for later access): " 6 .Addr cr
// multiSigInit$10 = MultiSigWrapper X;
<b 2 2 u, b>
// dup ."signing message: " <s csr. cr
// dup hash privkey ed25519_sign_uint
// Put StateInit on top
swap
// message$_ {X:Type} info:CommonMsgInfo
// init:(Maybe (Either StateInit ^StateInit))
// body:(Either X ^X) = Message X;
<b b{1000100} s, wallet-addr addr, 0 Gram, 1 1 u, swap maybe-ref, b{0} s, swap <s s, b>
dup ."External message for initialization is " <s csr. cr
2 boc+>B dup Bx. cr
file-base +"-create.boc" tuck B>file
."(Saved wallet creating query to file " type .")" cr

128
lightning/wallet-update.fif Normal file
View File

@ -0,0 +1,128 @@
"TonUtil.fif" include
"lib.fif" include
{ ."usage: " @' $0 type ." <wallet-name> <wallet-update> <seqno> <n> <k> <key-id> <privkey1> [<pubkey2> ...]" cr cr
."Updates an existing multisignature wallet." cr
."The first of the keys must be a private key (with ID <key-id>), used to sign the wallet update request saved to <wallet-update>.boc; the rest MUST be public keys." cr
."Create or generate public key files from private keys using gen-pub.fif privkey" cr cr
."Min <k> (1-10) signatures required to send an order; load <n> pre-existing public keys from files <key1...n>." cr
1 halt
} : usage
$# 6 < ' usage if
$1 constant file-base
$2 constant update-file
$3 parse-int constant msg-seqno
$4 (number) 1 <> abort"<n> must be a number!" constant n
$5 (number) 1 <> abort"<k> must be a number!" constant k
$6 (number) 1 <> abort"<k> must be a number!" constant key-id
file-base +".addr" load-address
2dup 2constant wallet-addr
."Source wallet address = " 2dup .addr cr 6 .Addr cr
3 constant send-mode // mode for SENDRAWMSG: +1 - sender pays fees, +2 - ignore errors
3 86400 * constant timeout // external message expires in 3 days
n 1 < n 10 > or abort"<n> must be between 1 and 10"
k 1 < k 10 > or abort"<k> must be between 1 and 10"
k n <= not abort"<k> must smaller than or equal to <n>"
$# 6 n + < abort"Not enough keys were provided in args!"
$7 +".pk" load-generate-keypair const privkey 256 B>u@
8 { dup $() +".pubkey" load-pubkey swap 1+ } n 1- times drop
n tuple constant keys
cr
."Updating pre-existing multisignature wallet with n=" n .
."k=" k . ."..." cr cr
// idict! (v x s n s0 1 or s 0), adds a new value v (represented
// by a Slice) with key given by signed big-endian n-bit integer x into
// dictionary s with n-bit keys, and returns the new dictionary s0 and 1
// on success. Otherwise the unchanged dictionary s and 0 are returned.
// Create dictionaries with keys and messages
// Extract keys
keys explode
dup 1- // Create counter
dictnew swap // ...and dict (swap the two)
rot // Put length on top for times
{ dup 1- swap // Decrement counter
3 roll // Get n-th value v (val dict ncount curcount)
<b swap 256 u, // Create builder bval
swap // Get x (dict ncount curcount bval)
3 roll // Get dictionary s (dict ncount bval curcount)
4 // Get n (ncount bval curcount dict) 4
b>udict!
not abort"Failure storing dictionary value!"
swap
} swap times drop const keys-dict
// code
"wallet-code-update.fif" include
// Create code message
// codeMessage$1 minSigs:(## 4) keys:(HashmapE 4 PubKey) code:^Cell = ModeMessage X;
<b 1 1 u,
k 4 u,
keys-dict dict,
swap ref,
b>
// create wrapper message
// wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X;
<b now timeout + 32 u, msg-seqno 32 u, swap <s s, b>
dup ."signing message: " <s csr. cr
dup hashu
dup ."Hash: " x. cr cr
privkey ed25519_sign_uint
// signature$_ R:bits256 s:bits256 = Signature;
256 B>u@+ swap 256 B>u@ swap
<b swap 256 u, swap 256 u, b>
// key ID => signature
// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X;
<b
0 1 u, // $0
swap <s key-id dictnew 4 udict! // Create and populate dictionary
not abort"Failure inserting signature!"
dict, // signatures
swap <s s, // message
b>
//
// addr_none$00 = MsgAddressExt;
// addr_extern$01 len:(## 9) external_address:(bits len)
// = MsgAddressExt;
// anycast_info$_ depth:(#<= 30) { depth >= 1 }
// rewrite_pfx:(bits depth) = Anycast;
// addr_std$10 anycast:(Maybe Anycast)
// workchain_id:int8 address:bits256 = MsgAddressInt;
// addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9)
// workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
//
// ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
// import_fee:Grams = CommonMsgInfo;
//
//
// message$_ {X:Type} info:CommonMsgInfo
// init:(Maybe (Either StateInit ^StateInit))
// body:(Either X ^X) = Message X;
//
<b b{1000100} s, wallet-addr addr, 0 Gram, 0 1 u,
swap maybe-ref, b>
dup ."resulting external message: " <s csr. cr
2 boc+>B dup Bx. cr
update-file +".boc" tuck B>file
."Query expires in " timeout . ."seconds" cr
."(Saved to file " type .")" cr

View File

@ -10,7 +10,7 @@ I created a full testing platform with a collator emulator in FIFT that parses t
I've tested throughly the smart contract using my local collator emulator (`test.fif`), and the actual TON collator using a slightly tweaked [zerostate generator](https://github.com/ton-blockchain/ton/pull/145) and the custom `test-collator.sh` script.
I had loads of fun working with funC and especially fift, and I'm really looking forward to building stuff on TON; especially TON services on the P2P ADNL network, I'll start by adding support for the ADNL protocol in my MTProto client, [MadelineProto](https://github.com/danog/MadelineProto); but I'll also experiment more with the TON blockchain, creating s'more smart contracts.
I had loads of fun working with funC and especially fift (after the first moment of confusion, I really enjoyed working with it due to the absolute control on the memory it allows), and I'm really looking forward to building stuff on TON; especially TON services on the P2P ADNL network, I'll start by adding support for the ADNL protocol in my MTProto client, [MadelineProto](https://github.com/danog/MadelineProto); but I'll also experiment more with the TON blockchain, creating s'more smart contracts.
## Project structure

View File

@ -1,4 +1,4 @@
// This is just an approximated TL scheme, explaining the message formats used by the multisig wallet.
// This is a TL scheme explaining the message formats used by the multisig wallet.
// Multisig wallet constructors
// 256 bits
@ -20,14 +20,14 @@ multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) =
multiSigInit$10 = MultiSigWrapper X;
//multiSigFuture$11 = MultiSigWrapper X;
// For internal storage, no constructor ID
// For internal storage, no constructor ID and number of signatures per message
multiSigWrapperStorage$_ count:(## 4) signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapperStorage X;
// Not doing explicit versioning here, since the structure of the storage can only change after an update of the onchain wallet code
// Max 10 keys (not sure about this, the contest instruction file says 10 in one place and 100 in another, so I chose 10 => 16 => 2^(4) )
// Max 10 keys (not sure about this, the contest instruction file says 10 in one place and 100 in another, so I chose 10 => 16 => 2^(4))
//
// In this case, HashmapE n X types are used (instead of ^X), since values in hashmap are already stored as references (each fork has two references to further hashmasp)
// In this case, HashmapE n X types are used (instead of ^X), since values in hashmap are already stored as references (each fork has two references to further hashmaps)
// ...and 32 + 4 + (~2+8+256 bits for messages assuming long label) + (~1+4+4 bits for keys assuming unary label), still less than 1023
storage$_ seqno:uint32 minSigs:(## 4) keys:(HashmapE 4 PubKey) messages:(HashmapE 256 (MultiSigWrapperStorage X))
{ minSigs > 0 } { n >= minSigs } { n <= 10 } { minSigs <= 10 } = Storage X;

View File

@ -1,14 +1,16 @@
;; Multisig wallet smart contract (Daniil Gentili @danogentili <daniil@daniil.it>)
;; (Now unused) tuple manipulation primitives for integers
tuple tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
(tuple, ()) ~tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
;;
;; tuple tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
;; (tuple, ()) ~tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
;; Tuple manipulation primitives for cells
tuple tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
(tuple, ()) ~tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
;;
;; tuple tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
;; (tuple, ()) ~tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
int tuple_len(tuple t) asm "TLEN";
;; int tuple_len(tuple t) asm "TLEN";
cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
@ -37,10 +39,6 @@ int udict_has?(cell dict, int key_len, int index) asm(index dict key_len) "DICTU
return (messages, ());
}
() store_db(int seqno, cell keys, cell messages) {
set_data(begin_cell().store_uint(seqno, 32).store_dict(keys).store_dict(messages).end_cell());
}
() recv_internal(slice message) impure {
;; do nothing for internal messages
}

View File

@ -2,7 +2,6 @@
// automatically generated from `/home/daniil/repos/contest/lib/crypto/smartcont/stdlib.fc` `wallet-code-update.fc`
PROGRAM{
DECLPROC ~collect_garbage
DECLPROC store_db
DECLPROC recv_internal
DECLPROC recv_external
85143 DECLMETHOD seqno
@ -48,15 +47,6 @@ PROGRAM{
}>
DROP
}>
store_db PROC:<{
NEWC
s1 s3 XCHG
32 STU
STDICT
STDICT
ENDC
c4 POP
}>
recv_internal PROC:<{
DROP
}>

View File

@ -1,14 +1,16 @@
;; Multisig wallet smart contract (Daniil Gentili @danogentili <daniil@daniil.it>)
;; (Now unused) tuple manipulation primitives for integers
tuple tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
(tuple, ()) ~tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
;;
;; tuple tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
;; (tuple, ()) ~tuple_set(tuple t, int index, int value) asm(t value index) "SETINDEXVARQ";
;; Tuple manipulation primitives for cells
tuple tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
(tuple, ()) ~tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
;;
;; tuple tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
;; (tuple, ()) ~tuple_setcell(tuple t, int index, cell value) asm(t value index) "SETINDEXVARQ";
int tuple_len(tuple t) asm "TLEN";
;; int tuple_len(tuple t) asm "TLEN";
cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
@ -37,10 +39,6 @@ int udict_has?(cell dict, int key_len, int index) asm(index dict key_len) "DICTU
return (messages, ());
}
() store_db(int seqno, cell keys, cell messages) {
set_data(begin_cell().store_uint(seqno, 32).store_dict(keys).store_dict(messages).end_cell());
}
() recv_internal(slice message) impure {
;; do nothing for internal messages
}

View File

@ -2,7 +2,6 @@
// automatically generated from `/home/daniil/repos/contest/lib/crypto/smartcont/stdlib.fc` `wallet-code.fc`
PROGRAM{
DECLPROC ~collect_garbage
DECLPROC store_db
DECLPROC recv_internal
DECLPROC recv_external
85143 DECLMETHOD seqno
@ -47,15 +46,6 @@ PROGRAM{
}>
DROP
}>
store_db PROC:<{
NEWC
s1 s3 XCHG
32 STU
STDICT
STDICT
ENDC
c4 POP
}>
recv_internal PROC:<{
DROP
}>