From d826632e3f37f217a10490a62ded0d48c1d78e5b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 15 Oct 2019 16:17:12 +0200 Subject: [PATCH] Commit --- lightning/README.md | 202 ++++++++++++++++ lightning/create.fif | 92 +++++++ lightning/gen-pub.fif | 15 ++ lightning/inspect.fif | 9 + lightning/lib.fif | 235 ++++++++++++++++++ lightning/merge.fif | 53 +++++ lightning/proto/old.fif | 55 +++++ lightning/proto/scheme.tlb | 183 ++++++++++++++ lightning/sign.fif | 50 ++++ lightning/test-collator.sh | 22 ++ lightning/test-update.sh | 48 ++++ lightning/test.fif | 114 +++++++++ lightning/test.sh | 57 +++++ lightning/verify.fif | 41 ++++ lightning/wallet-code-update.fc | 236 ++++++++++++++++++ lightning/wallet-code-update.fif | 397 +++++++++++++++++++++++++++++++ lightning/wallet-code.fc | 217 +++++++++++++++++ lightning/wallet-code.fif | 330 +++++++++++++++++++++++++ lightning/wallet-create.fif | 100 ++++++++ lightning/wallet-update.fif | 128 ++++++++++ wallet/README.md | 2 +- wallet/proto/scheme.tlb | 8 +- wallet/wallet-code-update.fc | 16 +- wallet/wallet-code-update.fif | 10 - wallet/wallet-code.fc | 16 +- wallet/wallet-code.fif | 10 - 26 files changed, 2603 insertions(+), 43 deletions(-) create mode 100644 lightning/README.md create mode 100755 lightning/create.fif create mode 100644 lightning/gen-pub.fif create mode 100644 lightning/inspect.fif create mode 100644 lightning/lib.fif create mode 100644 lightning/merge.fif create mode 100644 lightning/proto/old.fif create mode 100644 lightning/proto/scheme.tlb create mode 100644 lightning/sign.fif create mode 100755 lightning/test-collator.sh create mode 100755 lightning/test-update.sh create mode 100644 lightning/test.fif create mode 100755 lightning/test.sh create mode 100644 lightning/verify.fif create mode 100644 lightning/wallet-code-update.fc create mode 100644 lightning/wallet-code-update.fif create mode 100644 lightning/wallet-code.fc create mode 100644 lightning/wallet-code.fif create mode 100644 lightning/wallet-create.fif create mode 100644 lightning/wallet-update.fif diff --git a/lightning/README.md b/lightning/README.md new file mode 100644 index 0000000..ecde90b --- /dev/null +++ b/lightning/README.md @@ -0,0 +1,202 @@ +# Multisignature wallet + +Daniil Gentili's submission (@danogentili, ). + +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 [ ... ] + ``` + + Runs the function with ID in the TON VM, using initial code and storage data from .boc and .boc as message (OR as the only integer parameter of get-method, if .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 + ``` + + Create public key files from private keys; if `` 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 [ ...] + ``` + + Creates a new multisignature wallet in specified workchain composed of `` (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 `` (1-10) signatures required to send an order; load `` pre-existing public keys from files ``. + +* `wallet-update.fif` - Creates wallet code update request + ``` + usage: wallet-update.fif [ ...] + ``` + + Updates an existing multisignature wallet. + The first of the keys must be a private key (with ID ``), used to sign the wallet update request saved to `.boc`; the rest MUST be public keys. + Create or generate public key files from private keys using gen-pub.fif privkey + + Min `` (1-10) signatures required to send an order; load `` pre-existing public keys from files ``. + +* `create.fif` - Creates simple message to be sent to wallet + ``` + usage: create.fif [-B ] [] + ``` + + Creates a request to shared wallet created by wallet-create.fif, with private key `` loaded from file `.pk` and address from `.addr`, and saves it into `.boc` ('wallet-query.boc' by default) + +* `sign.fif` - Adds signature to wallet message + ``` + usage: sign.fif + ``` + + Signs multisig `.boc` file with private key `` loaded from file `.pk` and writes result to `.boc` + +* `verify.fif` - Verifies signature of message against known public key(s) + ``` + usage: verify.fif [ ...] + ``` + + Verify multisig `.boc` file with public key `` loaded from file `.pubkey` + +* `merge.fif` - Merges multiple messages with same content and different signatures + ``` + usage: merge.fif [ ...] + ``` + + Merges multisig `.boc` files and writes result to `.boc` + +* `inspect.fif` - Inspects the contents of request + ``` + usage: inspect.fif + ``` + + Inspects contents of multisig `.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 \ No newline at end of file diff --git a/lightning/create.fif b/lightning/create.fif new file mode 100755 index 0000000..1f82a1b --- /dev/null +++ b/lightning/create.fif @@ -0,0 +1,92 @@ +#!/usr/bin/env -S fift -s +"TonUtil.fif" include + +{ + ."usage: " @' $0 type ." [-B ] []" cr + ."Creates a request to shared wallet created by wallet-create.fif, with private key loaded from file .pk " + ."and address from .addr, and saves it into .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 } { } 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 +// create wrapper message +// modeMessage$0 mode:uint8 body:^(Message X) = ModeMessage X; +// wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X; + + +dup ."signing message: " u@+ swap 256 B>u@ swap + + +// key ID => signature +// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X; + + +// +// 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; +// + + +dup ."resulting external message: " B dup Bx. cr +savefile +".boc" tuck B>file +."Query expires in " timeout . ."seconds" cr +."(Saved to file " type .")" cr diff --git a/lightning/gen-pub.fif b/lightning/gen-pub.fif new file mode 100644 index 0000000..76b68b3 --- /dev/null +++ b/lightning/gen-pub.fif @@ -0,0 +1,15 @@ +"TonUtil.fif" include + +{ ."usage: " @' $0 type ." " cr + ."Create public key files from private keys; if 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 \ No newline at end of file diff --git a/lightning/inspect.fif b/lightning/inspect.fif new file mode 100644 index 0000000..e502f5b --- /dev/null +++ b/lightning/inspect.fif @@ -0,0 +1,9 @@ +{ + ."usage: " @' $0 type ." " cr + ."Inspects contents of multisig .boc file." cr 1 halt +} : usage +$# 1 < ' usage if + +"lib.fif" include + +$1 +".boc" load-boc inspect diff --git a/lightning/lib.fif b/lightning/lib.fif new file mode 100644 index 0000000..b9af926 --- /dev/null +++ b/lightning/lib.fif @@ -0,0 +1,235 @@ +"TonUtil.fif" include + +// b body -- b' +{ tuck 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: " " cr ref@ 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; + // + // + // (external message) + // + + + // message$_ {X:Type} info:CommonMsgInfo + // init:(Maybe (Either StateInit ^StateInit)) + // body:(Either X ^X) = Message X; + 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@ [ ...] " cr + ."Merges multisig .boc files and writes result to .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; + + +// Renerate external message + + +2 boc+>B dup Bx. cr +output-file tuck B>file +."Saved new multisigned message to file " type cr diff --git a/lightning/proto/old.fif b/lightning/proto/old.fif new file mode 100644 index 0000000..7c861c1 --- /dev/null +++ b/lightning/proto/old.fif @@ -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 + not and { 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 diff --git a/lightning/proto/scheme.tlb b/lightning/proto/scheme.tlb new file mode 100644 index 0000000..8396d40 --- /dev/null +++ b/lightning/proto/scheme.tlb @@ -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; + diff --git a/lightning/sign.fif b/lightning/sign.fif new file mode 100644 index 0000000..1bbff72 --- /dev/null +++ b/lightning/sign.fif @@ -0,0 +1,50 @@ +#!/usr/bin/env -S fift -s +"TonUtil.fif" include +"lib.fif" include + +{ + ."usage: " @' $0 type ." " cr + ."Signs multisig .boc file with private key loaded from file .pk and writes result to .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: " u@+ swap 256 B>u@ swap + 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; + + +// Renerate external message + + +2 boc+>B dup Bx. cr +output-file +".boc" tuck B>file +."Saved new multisigned message to file " type cr diff --git a/lightning/test-collator.sh b/lightning/test-collator.sh new file mode 100755 index 0000000..b4f0fc2 --- /dev/null +++ b/lightning/test-collator.sh @@ -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 diff --git a/lightning/test-update.sh b/lightning/test-update.sh new file mode 100755 index 0000000..52610d9 --- /dev/null +++ b/lightning/test-update.sh @@ -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 diff --git a/lightning/test.fif b/lightning/test.fif new file mode 100644 index 0000000..a510047 --- /dev/null +++ b/lightning/test.fif @@ -0,0 +1,114 @@ +"TonUtil.fif" include +"lib.fif" include + +{ + ."usage: " @' $0 type ." [ ... ]" cr + ."Runs the function with ID in the TON VM, using code and storage data from .boc and .boc as message (OR as the only integer parameter of get-method, if .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 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 { ."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@ 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 + // + 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 \ No newline at end of file diff --git a/lightning/test.sh b/lightning/test.sh new file mode 100755 index 0000000..893ba1e --- /dev/null +++ b/lightning/test.sh @@ -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 diff --git a/lightning/verify.fif b/lightning/verify.fif new file mode 100644 index 0000000..2df04ee --- /dev/null +++ b/lightning/verify.fif @@ -0,0 +1,41 @@ +#!/usr/bin/env -S fift -s +"TonUtil.fif" include +"lib.fif" include + +{ + ."usage: " @' $0 type ." [ ...]" cr + ."Verify multisig .boc file with public key loaded from file .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 \ No newline at end of file diff --git a/lightning/wallet-code-update.fc b/lightning/wallet-code-update.fc new file mode 100644 index 0000000..1ae8770 --- /dev/null +++ b/lightning/wallet-code-update.fc @@ -0,0 +1,236 @@ +;; Multisig wallet smart contract (Daniil Gentili @danogentili ) + +;; (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); +} diff --git a/lightning/wallet-code-update.fif b/lightning/wallet-code-update.fif new file mode 100644 index 0000000..2e9bd8e --- /dev/null +++ b/lightning/wallet-code-update.fif @@ -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 diff --git a/lightning/wallet-code.fc b/lightning/wallet-code.fc new file mode 100644 index 0000000..a672c49 --- /dev/null +++ b/lightning/wallet-code.fc @@ -0,0 +1,217 @@ +;; Multisig wallet smart contract (Daniil Gentili @danogentili ) + +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()); +} \ No newline at end of file diff --git a/lightning/wallet-code.fif b/lightning/wallet-code.fif new file mode 100644 index 0000000..3a4f4e0 --- /dev/null +++ b/lightning/wallet-code.fif @@ -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 diff --git a/lightning/wallet-create.fif b/lightning/wallet-create.fif new file mode 100644 index 0000000..fe8fe5f --- /dev/null +++ b/lightning/wallet-create.fif @@ -0,0 +1,100 @@ +"TonUtil.fif" include +"lib.fif" include + +{ ."usage: " @' $0 type ." [ ...]" cr cr + ."Creates a new multisignature wallet in specified workchain composed of (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 (1-10) signatures required to send an order; load pre-existing public keys from files ." 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" must be a number!" constant n +$4 (number) 1 <> abort" must be a number!" constant k + +n 1 < n 10 > or abort" must be between 1 and 10" +k 1 < k 10 > or abort" must be between 1 and 10" +k n <= not abort" must smaller than or equal to " + +$# 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) + 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; + +// 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 +// + + +dup ."StateInit: " +// dup ."signing message: " +dup ."External message for initialization is " B dup Bx. cr +file-base +"-create.boc" tuck B>file +."(Saved wallet creating query to file " type .")" cr diff --git a/lightning/wallet-update.fif b/lightning/wallet-update.fif new file mode 100644 index 0000000..c56b369 --- /dev/null +++ b/lightning/wallet-update.fif @@ -0,0 +1,128 @@ +"TonUtil.fif" include +"lib.fif" include + +{ ."usage: " @' $0 type ." [ ...]" cr cr + ."Updates an existing multisignature wallet." cr + ."The first of the keys must be a private key (with ID ), used to sign the wallet update request saved to .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 (1-10) signatures required to send an order; load pre-existing public keys from files ." 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" must be a number!" constant n +$5 (number) 1 <> abort" must be a number!" constant k +$6 (number) 1 <> abort" 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" must be between 1 and 10" +k 1 < k 10 > or abort" must be between 1 and 10" +k n <= not abort" must smaller than or equal to " + +$# 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) + 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; + +// create wrapper message +// wrappedMessage$_ expires_at:uint32 seqno:uint32 body:(ModeMessage X) = WrappedMessage X; + + +dup ."signing message: " u@+ swap 256 B>u@ swap + + +// key ID => signature +// multiSigWrapper$0 signatures:(HashmapE 4 Signature) message:(WrappedMessage X) = MultiSigWrapper X; + + + +// +// 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; +// + + +dup ."resulting external message: " B dup Bx. cr +update-file +".boc" tuck B>file +."Query expires in " timeout . ."seconds" cr +."(Saved to file " type .")" cr diff --git a/wallet/README.md b/wallet/README.md index 0a8c06b..ecde90b 100644 --- a/wallet/README.md +++ b/wallet/README.md @@ -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 diff --git a/wallet/proto/scheme.tlb b/wallet/proto/scheme.tlb index 3870490..a83861b 100644 --- a/wallet/proto/scheme.tlb +++ b/wallet/proto/scheme.tlb @@ -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; diff --git a/wallet/wallet-code-update.fc b/wallet/wallet-code-update.fc index 1ae8770..3ff3d36 100644 --- a/wallet/wallet-code-update.fc +++ b/wallet/wallet-code-update.fc @@ -1,14 +1,16 @@ ;; Multisig wallet smart contract (Daniil Gentili @danogentili ) ;; (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 } diff --git a/wallet/wallet-code-update.fif b/wallet/wallet-code-update.fif index 2e9bd8e..7f3a0e9 100644 --- a/wallet/wallet-code-update.fif +++ b/wallet/wallet-code-update.fif @@ -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 }> diff --git a/wallet/wallet-code.fc b/wallet/wallet-code.fc index 01bc34a..4cf4954 100644 --- a/wallet/wallet-code.fc +++ b/wallet/wallet-code.fc @@ -1,14 +1,16 @@ ;; Multisig wallet smart contract (Daniil Gentili @danogentili ) ;; (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 } diff --git a/wallet/wallet-code.fif b/wallet/wallet-code.fif index b5b4bcb..f2663d2 100644 --- a/wallet/wallet-code.fif +++ b/wallet/wallet-code.fif @@ -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 }>