diff --git a/README.md b/README.md index fcb74faa..e6f1f816 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ You can use each submodule individually. Click the module below to get more deta * ✅ Get block transactions * ✅ Get account transactions * ✅ Deploy contracts and send external messages using Tonlib -* ✅ Wallets - Simple (V1), V2, V3, V4 (plugins), Lockup, Highload, DNS, Jetton, NFT, Payment-channels +* ✅ Wallets - Simple (V1), V2, V3, V4 (plugins), Lockup, Highload, DNS, Jetton, NFT, Payment-channels, Multisig * ✅ HashMap, HashMapE, PfxHashMap and PfxHashMapE serialization / deserialization ### Todo diff --git a/smartcontract/README.md b/smartcontract/README.md index 1f958ebd..2f843228 100644 --- a/smartcontract/README.md +++ b/smartcontract/README.md @@ -43,6 +43,7 @@ Currently, following wallet versions and revisions are supported: * Jetton [(see usage example)](jetton-example.md) * NFT [(see usage example)](nft-example.md) * Payment channels [(see usage example)](./src/test/java/org/ton/java/smartcontract/integrationtests/TestPayments.java) +* Multisig [(see usage example)](./src/test/java/org/ton/java/smartcontract/integrationtests/TestWalletMultisig.java) * Custom contract [(see usage example)](custom-smc-example.md) diff --git a/smartcontract/src/main/java/org/ton/java/smartcontract/multisig/MultisigWallet.java b/smartcontract/src/main/java/org/ton/java/smartcontract/multisig/MultisigWallet.java index 0c302236..7479832b 100644 --- a/smartcontract/src/main/java/org/ton/java/smartcontract/multisig/MultisigWallet.java +++ b/smartcontract/src/main/java/org/ton/java/smartcontract/multisig/MultisigWallet.java @@ -5,7 +5,9 @@ import org.ton.java.address.Address; import org.ton.java.cell.*; import org.ton.java.smartcontract.types.ExternalMessage; +import org.ton.java.smartcontract.types.MultisigSignature; import org.ton.java.smartcontract.types.OwnerInfo; +import org.ton.java.smartcontract.types.PendingQuery; import org.ton.java.smartcontract.wallet.Contract; import org.ton.java.smartcontract.wallet.Options; import org.ton.java.smartcontract.wallet.WalletContract; @@ -19,10 +21,13 @@ import java.math.BigInteger; import java.util.*; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + public class MultisigWallet implements WalletContract { //https://github.com/akifoq/multisig/blob/master/multisig-code.fc - public static final String MULTISIG_WALLET_CODE_HEX = "B5EE9C7241022B0100041A000114FF00F4A413F4BCF2C80B010201200203020148040504DAF220C7008E8330DB3CE08308D71820F90101D307DB3C22C00013A1537178F40E6FA1F29FDB3C541ABAF910F2A006F40420F90101D31F5118BAF2AAD33F705301F00A01C20801830ABCB1F26853158040F40E6FA120980EA420C20AF2670EDFF823AA1F5340B9F2615423A3534E202321220202CC06070201200C0D02012008090201660A0B0003D1840223F2980BC7A0737D0986D9E52ED9E013C7A21C2125002D00A908B5D244A824C8B5D2A5C0B5007404FC02BA1B04A0004F085BA44C78081BA44C3800740835D2B0C026B500BC02F21633C5B332781C75C8F20073C5BD0032600201200E0F02012014150115BBED96D5034705520DB3C82A020148101102012012130173B11D7420C235C6083E404074C1E08075313B50F614C81E3D039BE87CA7F5C2FFD78C7E443CA82B807D01085BA4D6DC4CB83E405636CF0069006027003DAEDA80E800E800FA02017A0211FC8080FC80DD794FF805E47A0000E78B64C00019AE19573859C100D56676A1EC40020120161702012018190151B7255B678626466A4610081E81CDF431C24D845A4000331A61E62E005AE0261C0B6FEE1C0B77746E10230189B5599B6786ABE06FEDB1C6CA2270081E8F8DF4A411C4A05A400031C38410021AE424BAE064F6451613990039E2CA840090081E886052261C52261C52265C4036625CCD8A30230201201A1B0017B506B5CE104035599DA87B100201201C1D020399381E1F0111AC1A6D9E2F81B60940230015ADF94100CC9576A1EC1840010DA936CF0557C160230017ADDC2CDC20806AB33B50F6200220DB3C02F265F8005043714313DB3CED54232A000AD3FFD3073004A0DB3C2FAE5320B0F26212B102A425B3531CB9B0258100E1AA23A028BCB0F269820186A0F8010597021110023E3E308E8D11101FDB3C40D778F44310BD05E254165B5473E7561053DCDB3C54710A547ABC242528260020ED44D0D31FD307D307D33FF404F404D1005E018E1A30D20001F2A3D307D3075003D70120F90105F90115BAF2A45003E06C2121D74AAA0222D749BAF2AB70542013000C01C8CBFFCB0704D6DB3CED54F80F70256E5389BEB198106E102D50C75F078F1B30542403504DDB3C5055A046501049103A4B0953B9DB3C5054167FE2F800078325A18E2C268040F4966FA52094305303B9DE208E1638393908D2000197D3073016F007059130E27F080705926C31E2B3E630062A2728290060708E2903D08308D718D307F40430531678F40E6FA1F2A5D70BFF544544F910F2A6AE5220B15203BD14A1236EE66C2232007E5230BE8E205F03F8009322D74A9802D307D402FB0002E83270C8CA0040148040F44302F0078E1771C8CB0014CB0712CB0758CF0158CF1640138040F44301E201208E8A104510344300DB3CED54925F06E22A001CC8CB1FCB07CB07CB3FF400F400C984B5AC4C"; + public static final String MULTISIG_WALLET_CODE_HEX = "B5EE9C7241022B01000418000114FF00F4A413F4BCF2C80B010201200203020148040504DAF220C7008E8330DB3CE08308D71820F90101D307DB3C22C00013A1537178F40E6FA1F29FDB3C541ABAF910F2A006F40420F90101D31F5118BAF2AAD33F705301F00A01C20801830ABCB1F26853158040F40E6FA120980EA420C20AF2670EDFF823AA1F5340B9F2615423A3534E202321220202CC06070201200C0D02012008090201660A0B0003D1840223F2980BC7A0737D0986D9E52ED9E013C7A21C2125002D00A908B5D244A824C8B5D2A5C0B5007404FC02BA1B04A0004F085BA44C78081BA44C3800740835D2B0C026B500BC02F21633C5B332781C75C8F20073C5BD0032600201200E0F02012014150115BBED96D5034705520DB3C82A020148101102012012130173B11D7420C235C6083E404074C1E08075313B50F614C81E3D039BE87CA7F5C2FFD78C7E443CA82B807D01085BA4D6DC4CB83E405636CF0069006027003DAEDA80E800E800FA02017A0211FC8080FC80DD794FF805E47A0000E78B64C00017AE19573FC100D56676A1EC40020120161702012018190151B7255B678626466A4610081E81CDF431C24D845A4000331A61E62E005AE0261C0B6FEE1C0B77746E10230189B5599B6786ABE06FEDB1C6CA2270081E8F8DF4A411C4A05A400031C38410021AE424BAE064F6451613990039E2CA840090081E886052261C52261C52265C4036625CCD8A30230201201A1B0017B506B5CE104035599DA87B100201201C1D020399381E1F0111AC1A6D9E2F81B60940230015ADF94100CC9576A1EC1840010DA936CF0557C160230015ADDFDC20806AB33B50F6200220DB3C02F265F8005043714313DB3CED54232A000AD3FFD3073004A0DB3C2FAE5320B0F26212B102A425B3531CB9B0258100E1AA23A028BCB0F269820186A0F8010597021110023E3E308E8D11101FDB3C40D778F44310BD05E254165B5473E7561053DCDB3C54710A547ABC242528260020ED44D0D31FD307D307D33FF404F404D1005E018E1A30D20001F2A3D307D3075003D70120F90105F90115BAF2A45003E06C2121D74AAA0222D749BAF2AB70542013000C01C8CBFFCB0704D6DB3CED54F80F70256E5389BEB198106E102D50C75F078F1B30542403504DDB3C5055A046501049103A4B0953B9DB3C5054167FE2F800078325A18E2C268040F4966FA52094305303B9DE208E1638393908D2000197D3073016F007059130E27F080705926C31E2B3E630062A2728290060708E2903D08308D718D307F40430531678F40E6FA1F2A5D70BFF544544F910F2A6AE5220B15203BD14A1236EE66C2232007E5230BE8E205F03F8009322D74A9802D307D402FB0002E83270C8CA0040148040F44302F0078E1771C8CB0014CB0712CB0758CF0158CF1640138040F44301E201208E8A104510344300DB3CED54925F06E22A001CC8CB1FCB07CB07CB3FF400F400C9B99895F4"; Options options; Address address; @@ -65,30 +70,29 @@ public Address getAddress() { public Cell createDataCell() { CellBuilder cell = CellBuilder.beginCell(); - cell.storeUint(getOptions().getWalletId(), 32); // sub-wallet id + cell.storeUint(getOptions().getWalletId(), 32); cell.storeUint(getOptions().getMultisigConfig().getN(), 8); // n cell.storeUint(getOptions().getMultisigConfig().getK(), 8); // k - collect at least k signatures cell.storeUint(BigInteger.ZERO, 64); // last cleaned - cell.storeDict(createOwnersInfosDict(getOptions().getMultisigConfig().getOwners())); // initial owner infos dict, public keys - cell.storeBit(false); // initial pending queries dict + if (isNull(getOptions().getMultisigConfig().getOwners()) || getOptions().getMultisigConfig().getOwners().isEmpty()) { + cell.storeBit(false); // initial owners dict + } else { + cell.storeDict(createOwnersInfosDict(getOptions().getMultisigConfig().getOwners())); + } + + if (isNull(getOptions().getMultisigConfig().getPendingQueries()) || getOptions().getMultisigConfig().getPendingQueries().isEmpty()) { + cell.storeBit(false); // initial pending queries dict + } else { + cell.storeDict(createPendingQueries(getOptions().getMultisigConfig().getPendingQueries(), getOptions().getMultisigConfig().getN())); + } return cell.endCell(); } - public Cell createSigningMessageInternal(int pubkeyIndex, List signatures, Cell order, BigInteger queryId) { - + private Cell createSigningMessageInternal(int pubkeyIndex, Cell order) { CellBuilder message = CellBuilder.beginCell(); message.storeUint(pubkeyIndex, 8); // root-id - pk-index for owner_infos dict - - message.storeBit(true); // sigs dict exists and not empty - works ok - - message.storeUint(getOptions().getWalletId(), 32); // wallet-id - message.storeUint(queryId, 64); // query-id - - message.storeRef(serializeSignatures(signatures.size(), 0, signatures)); // works - message.writeCell(order); - return message.endCell(); } @@ -135,9 +139,9 @@ public List getPublicKeysHex(Tonlib tonlib) { * * @param tonlib tonlib * @param walletId walletid - * @param n - total keys - * @param k - minimum number of keys - * @param ownersInfo - arrays with public keys + * @param n total keys + * @param k minimum number of keys + * @param ownersInfo arrays with public keys * @return cell with state-init */ public Cell getInitState(Tonlib tonlib, long walletId, int n, int k, Cell ownersInfo) { @@ -160,18 +164,35 @@ public Cell getInitState(Tonlib tonlib, long walletId, int n, int k, Cell owners } /** - * Sends to up to 84 destinations + * Sends an external msg with the order containing all collected signatures signed by owner at index pubkeyIndex with keyPair. * - * @param tonlib Tonlib - * @param keyPair TweetNaclFast.Signature.KeyPair - * @param signatures List + * @param tonlib Tonlib + * @param keyPair TweetNaclFast.Signature.KeyPair */ - public void sendOrder(Tonlib tonlib, TweetNaclFast.Signature.KeyPair keyPair, int pubkeyIndex, Cell order, BigInteger queryId, List signatures) { - Cell signingMessageBody = createSigningMessageInternal(pubkeyIndex, signatures, order, queryId); + public void sendOrder(Tonlib tonlib, TweetNaclFast.Signature.KeyPair keyPair, int pubkeyIndex, Cell order) { + Cell signingMessageBody = createSigningMessageInternal(pubkeyIndex, order); ExternalMessage msg = createExternalMessage(signingMessageBody, keyPair.getSecretKey(), 1, false); tonlib.sendRawMessage(msg.message.toBocBase64(false)); } + /** + * Sends an external msg with the order containing all collected signatures signed by owner at index pubkeyIndex with secretKey. + * + * @param tonlib Tonlib + * @param secretKey byte[] + */ + public void sendOrder(Tonlib tonlib, byte[] secretKey, int pubkeyIndex, Cell order) { + Cell signingMessageBody = createSigningMessageInternal(pubkeyIndex, order); + ExternalMessage msg = createExternalMessage(signingMessageBody, secretKey, 1, false); + tonlib.sendRawMessage(msg.message.toBocBase64(false)); + } + + /** + * Serializes list of multisig wallet owners. + * + * @param ownerInfos OwnerInfo + * @return Cell + */ public Cell createOwnersInfosDict(List ownerInfos) { int dictKeySize = 8; TonHashMapE dictDestinations = new TonHashMapE(dictKeySize); @@ -197,35 +218,99 @@ public Cell createOwnersInfosDict(List ownerInfos) { return cellDict; } - public Cell serializeSignatures(int total, int i, List signatures) { + public static Cell createPendingQueries(List pendingQueries, int n) { + int dictKeySize = 64; + TonHashMapE dictDestinations = new TonHashMapE(dictKeySize); + + long i = 0; // key, index 16bit + for (PendingQuery query : pendingQueries) { + + CellBuilder queryCell = CellBuilder.beginCell(); + queryCell.storeBit(true); + queryCell.storeUint(query.getCreatorI(), 8); + queryCell.storeUint(query.getCnt(), 8); + queryCell.storeUint(query.getCntBits(), n); + queryCell.writeCell(query.getMsg()); + + dictDestinations.elements.put( + query.getQueryId(), // key - query-id + queryCell.endCell() // value - cell - QueryData + ); + } + + Cell cellDict = dictDestinations.serialize( + k -> CellBuilder.beginCell().storeUint((BigInteger) k, dictKeySize).bits, + v -> (Cell) v + ); + + return cellDict; + } + + public static Cell createSignaturesDict(List signatures) { + int dictKeySize = 8; // what is the size of the key? + TonHashMapE dictSignatures = new TonHashMapE(dictKeySize); + + long i = 0; // key, index + for (byte[] signature : signatures) { + + CellBuilder sigCell = CellBuilder.beginCell(); + sigCell.storeBytes(signature); + sigCell.storeUint(i, 8); + + dictSignatures.elements.put( + i, // key - index + sigCell.endCell() // value - cell - Signature, 512+8 + ); + i++; + } + + Cell cellDict = dictSignatures.serialize( + k -> CellBuilder.beginCell().storeUint((Long) k, dictKeySize).bits, + v -> (Cell) v + ); + + return cellDict; + } + + /** + * Serialized list of signatures into cell + * + * @param i start index + * @param signatures list of signatures + * @return Cell + */ + public static Cell serializeSignatures(int i, List signatures) { CellBuilder c = CellBuilder.beginCell(); - c.storeBytes(signatures.get(i)); - c.storeUint(i, 8); - if (i == total - 1) { + c.storeBytes(signatures.get(i).getSignature()); + c.storeUint(signatures.get(i).getPubKeyPosition(), 8); + if (i == signatures.size() - 1) { c.storeBit(false); // empty dict, last cell } else { c.storeBit(true); - c.storeRef(serializeSignatures(total, ++i, signatures)); + c.storeRef(serializeSignatures(++i, signatures)); } return c; } - public Cell createQuery(TweetNaclFast.Signature.KeyPair keyPair, List signatures, Cell order) { + public static Cell createQuery(TweetNaclFast.Signature.KeyPair keyPair, List signatures, Cell order) { CellBuilder rootCell = CellBuilder.beginCell(); rootCell.storeUint(0, 8); // root-i - if (signatures.isEmpty()) { - rootCell.storeBit(false); // empty dict + if (isNull(signatures) || signatures.isEmpty()) { + rootCell.storeBit(false); } else { - rootCell.storeBit(true); // not empty dict - rootCell.storeRef(serializeSignatures(signatures.size(), 0, signatures)); + rootCell.storeBit(true); + rootCell.storeRef(serializeSignatures(0, signatures)); } - rootCell.writeCell(order); + CellSlice cs = CellSlice.beginParse(order); + cs.skipBit(); // remove no-signatures flag + CellBuilder o = CellBuilder.beginCell(); + o.writeCell(cs.sliceToCell()); - byte[] rootSignature = signCell(keyPair, rootCell.endCell()); + rootCell.writeCell(o); - // todo check if our signature already exist inside order + byte[] rootSignature = signCell(keyPair, rootCell.endCell()); CellBuilder query = CellBuilder.beginCell(); query.storeBytes(rootSignature); @@ -250,12 +335,12 @@ public void deploy(Tonlib tonlib, byte[] secretKey) { * @param mode send mode * @return Cell */ - public Cell createOneInternalMsg(Address destination, BigInteger amount, int mode) { + public static Cell createOneInternalMsg(Address destination, BigInteger amount, int mode) { Cell intMsgHeader = Contract.createInternalMessageHeader(destination, amount); Cell intMsgTransfer = Contract.createCommonMsgInfo(intMsgHeader); CellBuilder p = CellBuilder.beginCell(); - p.storeUint(mode, 8); // if 9, terminating vm with exit code 43 + p.storeUint(mode, 8); p.storeRef(intMsgTransfer); return p.endCell(); @@ -265,11 +350,28 @@ public Cell createOneInternalMsg(Address destination, BigInteger amount, int mod * @param internalMsgs List of Cells, where Cell is internal msg, defining target destinations with amounts * @return Cell Order */ - public Cell createOrder(Cell... internalMsgs) { + public static Cell createOrder(Long walletId, BigInteger queryId, Cell... internalMsgs) { + if (internalMsgs.length > 3) { + throw new Error("Order cannot contain more than 3 internal messages"); + } + CellBuilder order = CellBuilder.beginCell(); + order.storeBit(false); // no signatures + order.storeUint(walletId, 32); + order.storeUint(queryId, 64); + + for (Cell msg : internalMsgs) { + order.writeCell(msg); + } + return order.endCell(); + } + + public static Cell createOrder1(Long walletId, BigInteger queryId, Cell... internalMsgs) { if (internalMsgs.length > 3) { throw new Error("Order cannot contain more than 3 internal messages"); } CellBuilder order = CellBuilder.beginCell(); + order.storeUint(walletId, 32); + order.storeUint(queryId, 64); for (Cell msg : internalMsgs) { order.writeCell(msg); @@ -277,10 +379,120 @@ public Cell createOrder(Cell... internalMsgs) { return order.endCell(); } - public byte[] signCell(TweetNaclFast.Signature.KeyPair keyPair, Cell cell) { + public static Cell addSignatures(Cell order, List signatures) { + + CellBuilder signedOrder = CellBuilder.beginCell(); + signedOrder.storeBit(true); // contains signatures + signedOrder.storeRef(serializeSignatures(0, signatures)); + + CellSlice cs = CellSlice.beginParse(order); + cs.skipBit(); // remove no-signatures flag + CellBuilder o = CellBuilder.beginCell(); + o.writeCell(cs.sliceToCell()); + + signedOrder.writeCell(o.endCell()); + return signedOrder.endCell(); + } + + private static void checkIfSignatureExists(Cell order, byte[] signature) { + CellSlice cs = CellSlice.beginParse(order); + + if (cs.loadBit()) { //order contains signatures + Cell ref = cs.loadRef(); + while (nonNull(ref)) { + byte[] sig = CellSlice.beginParse(ref).loadBytes(512); + System.out.println("sig " + Utils.bytesToHex(signature)); + if (sig == signature) { + throw new Error("Your signature is already presented"); + } + if (ref.refs.size() != 0) { + ref = ref.refs.get(0); + } else { + ref = null; + } + } + } + } + + public static Cell addSignature1(Cell order, int pubkeyIndex, TweetNaclFast.Signature.KeyPair keyPair) { + + CellSlice cs = CellSlice.beginParse(order); + cs.skipBit(); // remove no-signatures flag + CellBuilder o = CellBuilder.beginCell(); + o.writeCell(cs.sliceToCell()); + + byte[] signature = signCell(keyPair, o); + + System.out.println("sig " + Utils.bytesToHex(signature)); + +// checkIfSignatureExists(order, signature); + + cs = CellSlice.beginParse(order); + if (!cs.loadBit()) { //order didn't have any signatures, add first signature + cs.skipBit(); // remove no-signatures flag + + CellBuilder signedOrder = CellBuilder.beginCell(); + signedOrder.storeBit(true); // contains signatures + + CellBuilder c = CellBuilder.beginCell(); + c.storeBytes(signature); + c.storeUint(pubkeyIndex, 8); + c.storeBit(false); // no more references, only one signature added + + signedOrder.storeRef(c.endCell()); + signedOrder.writeCell(o); + return signedOrder.endCell(); + } else { // order contains some signatures + Cell otherSignatures = cs.loadRef(); + + CellBuilder signedOrder = CellBuilder.beginCell(); + signedOrder.storeBit(true); // contains signatures + + CellBuilder c = CellBuilder.beginCell(); + c.storeBytes(signature); // add new signature + c.storeUint(pubkeyIndex, 8); + c.storeBit(true); // add other signatures + c.storeRef(otherSignatures); + + signedOrder.storeRef(c.endCell()); + signedOrder.writeCell(o); + return signedOrder.endCell(); + } + +// CellBuilder signedOrder = CellBuilder.beginCell(); +// signedOrder.storeBit(true); // contains signatures +// +// CellBuilder c = CellBuilder.beginCell(); +// c.storeBytes(signature); +// c.storeUint(pubkeyIndex, 8); +// c.storeBit(false); +// +// signedOrder.storeRef(c.endCell()); +// signedOrder.writeCell(o); + +// CellSlice cs = CellSlice.beginParse(order); +// cs.skipBit(); // remove no-signatures flag +// CellBuilder o = CellBuilder.beginCell(); +// o.writeCell(cs.sliceToCell()); +// signedOrder.writeCell(o.endCell()); + +// return null; + } + + + public static byte[] signCell(TweetNaclFast.Signature.KeyPair keyPair, Cell cell) { return new TweetNaclFast.Signature(keyPair.getPublicKey(), keyPair.getSecretKey()).detached(cell.hash()); } + public static byte[] signOrder(TweetNaclFast.Signature.KeyPair keyPair, Cell order) { + CellSlice cs = CellSlice.beginParse(order); + cs.skipBit(); // remove no-signatures flag + CellBuilder o = CellBuilder.beginCell(); + o.writeCell(cs.sliceToCell()); + + return signCell(keyPair, o); + } + public Pair getNandK(Tonlib tonlib) { Address myAddress = this.getAddress(); @@ -318,14 +530,6 @@ public Map getMessagesUnsigned(Tonlib tonlib) { TvmStackEntryCell entryCell = (TvmStackEntryCell) result.getStack().get(0); Cell cellDict = CellBuilder.fromBoc(Utils.base64ToBytes(entryCell.getCell().getBytes())); - // returns dict <64bits, cell query-data> -// query-data -// .store_uint(1, 1) -// .store_uint(creator_i, 8) -// .store_uint(cnt, 8) -// .store_uint(cnt_bits, n) -// .store_slice(msg)); - CellSlice cs = CellSlice.beginParse(cellDict); TonHashMap loadedDict = cs @@ -479,6 +683,24 @@ public Pair checkQuerySignatures(Tonlib tonlib, Cell query) { return Pair.of(cnt.getNumber().longValue(), mask.getNumber().longValue()); } + public Cell mergePendingQueries(Tonlib tonlib, Cell a, Cell b) { + + Address myAddress = this.getAddress(); + Deque stack = new ArrayDeque<>(); + + stack.offer("[cell, " + a.toHex(false) + "]"); + stack.offer("[cell, " + b.toHex(false) + "]"); + RunResult result = tonlib.runMethod(myAddress, "merge_inner_queries", stack); + + if (result.getExit_code() != 0) { + throw new Error("method merge_inner_queries, returned an exit code " + result.getExit_code()); + } + + TvmStackEntryCell entryCell = (TvmStackEntryCell) result.getStack().get(0); + + return CellBuilder.fromBoc(Utils.base64ToBytes(entryCell.getCell().getBytes())); + } + /** * Returns -1 for processed queries, 0 for unprocessed, 1 for unknown (forgotten) * diff --git a/smartcontract/src/main/java/org/ton/java/smartcontract/types/MultisigConfig.java b/smartcontract/src/main/java/org/ton/java/smartcontract/types/MultisigConfig.java index b87d6d35..d7faaa63 100644 --- a/smartcontract/src/main/java/org/ton/java/smartcontract/types/MultisigConfig.java +++ b/smartcontract/src/main/java/org/ton/java/smartcontract/types/MultisigConfig.java @@ -19,6 +19,7 @@ public class MultisigConfig { * Whitelist of allowed destinations */ public List owners; + public List pendingQueries; public long rootI; /** @@ -26,9 +27,9 @@ public class MultisigConfig { *

* E.g. n = 5, k = 3, means at least 3 out of 5 signatures must be collected */ - public long k; + public int k; /** * total amount of private kyes */ - public long n; + public int n; } diff --git a/smartcontract/src/main/java/org/ton/java/smartcontract/types/MultisigSignature.java b/smartcontract/src/main/java/org/ton/java/smartcontract/types/MultisigSignature.java new file mode 100644 index 00000000..4fa12827 --- /dev/null +++ b/smartcontract/src/main/java/org/ton/java/smartcontract/types/MultisigSignature.java @@ -0,0 +1,13 @@ +package org.ton.java.smartcontract.types; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString +public class MultisigSignature { + long pubKeyPosition; + byte[] signature; +} diff --git a/smartcontract/src/main/java/org/ton/java/smartcontract/types/OwnerInfo.java b/smartcontract/src/main/java/org/ton/java/smartcontract/types/OwnerInfo.java index 93300fbf..443c438f 100644 --- a/smartcontract/src/main/java/org/ton/java/smartcontract/types/OwnerInfo.java +++ b/smartcontract/src/main/java/org/ton/java/smartcontract/types/OwnerInfo.java @@ -10,5 +10,4 @@ public class OwnerInfo { byte[] publicKey; long flood; - } diff --git a/smartcontract/src/main/java/org/ton/java/smartcontract/types/PendingQuery.java b/smartcontract/src/main/java/org/ton/java/smartcontract/types/PendingQuery.java new file mode 100644 index 00000000..a74f3e87 --- /dev/null +++ b/smartcontract/src/main/java/org/ton/java/smartcontract/types/PendingQuery.java @@ -0,0 +1,21 @@ +package org.ton.java.smartcontract.types; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.ton.java.cell.Cell; + +import java.math.BigInteger; + +@Builder +@Getter +@ToString +public class PendingQuery { + BigInteger queryId; + long creatorI; + long cnt; // current number of collected confirmations + + // bits of length n, with active bit at position of public keys array. 101 - signed with pubkey[0] and pubkey[2] + long cntBits; + Cell msg; +} diff --git a/smartcontract/src/main/java/org/ton/java/smartcontract/wallet/WalletContract.java b/smartcontract/src/main/java/org/ton/java/smartcontract/wallet/WalletContract.java index b80551aa..1a02976a 100644 --- a/smartcontract/src/main/java/org/ton/java/smartcontract/wallet/WalletContract.java +++ b/smartcontract/src/main/java/org/ton/java/smartcontract/wallet/WalletContract.java @@ -136,7 +136,7 @@ default ExternalMessage createExternalMessage(Cell signingMessage, if (seqno == 0) { if (isNull(getOptions().publicKey)) { - TweetNaclFast.Signature.KeyPair keyPair = Utils.generateSignatureKeyPairFromSeed(secretKey); //) TweetNaclFast.Box.keyPair_fromSecretKey( + TweetNaclFast.Signature.KeyPair keyPair = Utils.generateSignatureKeyPairFromSeed(secretKey); getOptions().publicKey = keyPair.getPublicKey(); } StateInit deploy = createStateInit(); diff --git a/smartcontract/src/test/java/org/ton/java/smartcontract/TestFaucet.java b/smartcontract/src/test/java/org/ton/java/smartcontract/TestFaucet.java index e6c2809c..ff8aa774 100644 --- a/smartcontract/src/test/java/org/ton/java/smartcontract/TestFaucet.java +++ b/smartcontract/src/test/java/org/ton/java/smartcontract/TestFaucet.java @@ -14,6 +14,7 @@ import org.ton.java.tonlib.Tonlib; import org.ton.java.tonlib.types.AccountAddressOnly; import org.ton.java.tonlib.types.FullAccountState; +import org.ton.java.tonlib.types.VerbosityLevel; import org.ton.java.utils.Utils; import java.math.BigInteger; @@ -134,4 +135,13 @@ public void deployFaucetWallet() { .build(); tonlib.sendRawMessage(msg.message.toBocBase64(false)); } + + @Test + public void topUpAnyContract() throws InterruptedException { + Tonlib tonlib = Tonlib.builder() + .testnet(true) + .verbosityLevel(VerbosityLevel.DEBUG) + .build(); + TestFaucet.topUpContract(tonlib, Address.of("0QB0gEuvySej-7ZZBAdaBSydBB_oVYUUnp9Ciwm05kJsNKau"), Utils.toNano(5)); + } } \ No newline at end of file diff --git a/smartcontract/src/test/java/org/ton/java/smartcontract/integrationtests/TestWalletMultisig.java b/smartcontract/src/test/java/org/ton/java/smartcontract/integrationtests/TestWalletMultisig.java index ab79ed21..f49ade92 100644 --- a/smartcontract/src/test/java/org/ton/java/smartcontract/integrationtests/TestWalletMultisig.java +++ b/smartcontract/src/test/java/org/ton/java/smartcontract/integrationtests/TestWalletMultisig.java @@ -8,12 +8,9 @@ import org.junit.runners.JUnit4; import org.ton.java.address.Address; import org.ton.java.cell.Cell; -import org.ton.java.cell.CellSlice; import org.ton.java.smartcontract.TestFaucet; import org.ton.java.smartcontract.multisig.MultisigWallet; -import org.ton.java.smartcontract.types.MultisigConfig; -import org.ton.java.smartcontract.types.OwnerInfo; -import org.ton.java.smartcontract.types.WalletVersion; +import org.ton.java.smartcontract.types.*; import org.ton.java.smartcontract.wallet.Options; import org.ton.java.smartcontract.wallet.Wallet; import org.ton.java.tonlib.Tonlib; @@ -26,6 +23,8 @@ import java.util.Map; import java.util.Random; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + @Slf4j @RunWith(JUnit4.class) public class TestWalletMultisig { @@ -36,14 +35,15 @@ public class TestWalletMultisig { TweetNaclFast.Signature.KeyPair keyPair4 = Utils.generateSignatureKeyPair(); TweetNaclFast.Signature.KeyPair keyPair5 = Utils.generateSignatureKeyPair(); + /** + * Any user deploys a multisig wallet. + * Any user from the list creates an order and gathers all the required signatures, + * then sends the order to the wallet. + */ @Test - public void testWalletMultisig() throws InterruptedException { - - Tonlib tonlib = Tonlib.builder() - .testnet(true) - // .verbosityLevel(VerbosityLevel.DEBUG) - .build(); + public void testWalletMultisigOffline() throws InterruptedException { + Tonlib tonlib = Tonlib.builder().testnet(true).build(); log.info("pubKey0 {}", Utils.bytesToHex(ownerKeyPair.getPublicKey())); log.info("pubKey2 {}", Utils.bytesToHex(keyPair2.getPublicKey())); @@ -51,10 +51,10 @@ public void testWalletMultisig() throws InterruptedException { log.info("pubKey4 {}", Utils.bytesToHex(keyPair4.getPublicKey())); log.info("pubKey5 {}", Utils.bytesToHex(keyPair5.getPublicKey())); - BigInteger queryId = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 5 * 60L, 32)); + BigInteger queryId = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 2 * 60 * 60L, 32)); Long walletId = new Random().nextLong() & 0xffffffffL; - log.info("queryId {}, walletId {}", queryId.toString(10), walletId); + log.info("queryId {}, walletId {}", queryId, walletId); int rootIndex = 0; int pubkey2Index = 1; @@ -72,32 +72,30 @@ public void testWalletMultisig() throws InterruptedException { .k(k) .n(n) .rootI(rootIndex) - .owners(List.of( - OwnerInfo.builder() - .publicKey(ownerKeyPair.getPublicKey()) - .flood(1) - .build(), - OwnerInfo.builder() - .publicKey(keyPair2.getPublicKey()) - .flood(2) - .build(), - OwnerInfo.builder() - .publicKey(keyPair3.getPublicKey()) - .flood(3) - .build(), - OwnerInfo.builder() - .publicKey(keyPair4.getPublicKey()) - .flood(4) - .build(), - OwnerInfo.builder() - .publicKey(keyPair5.getPublicKey()) - .flood(5) - .build() - - )) - // todo initial pending query list - .build()) - + .owners( + List.of( + OwnerInfo.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .flood(1) + .build(), + OwnerInfo.builder() + .publicKey(keyPair2.getPublicKey()) + .flood(2) + .build(), + OwnerInfo.builder() + .publicKey(keyPair3.getPublicKey()) + .flood(3) + .build(), + OwnerInfo.builder() + .publicKey(keyPair4.getPublicKey()) + .flood(4) + .build(), + OwnerInfo.builder() + .publicKey(keyPair5.getPublicKey()) + .flood(5) + .build() + ) + ).build()) .build(); Wallet wallet = new Wallet(WalletVersion.multisig, options); @@ -109,14 +107,14 @@ public void testWalletMultisig() throws InterruptedException { log.info("non-bounceable address {}", nonBounceableAddress); log.info(" bounceable address {}", bounceableAddress); - // top up new wallet using test-faucet-wallet + // top up new wallet using test-faucet-wallet BigInteger balance = TestFaucet.topUpContract(tonlib, Address.of(nonBounceableAddress), Utils.toNano(5)); Utils.sleep(10, "topping up..."); log.info("new wallet {} balance: {}", contract.getName(), Utils.formatNanoValue(balance)); contract.deploy(tonlib, ownerKeyPair.getSecretKey()); - Utils.sleep(30, "deploying"); // with empty ext msg + Utils.sleep(30, "deploying"); // with empty ext-msg log.info("owners publicKeys {}", contract.getPublicKeys(tonlib)); log.info("owners publicKeysHex {}", contract.getPublicKeysHex(tonlib)); @@ -125,90 +123,234 @@ public void testWalletMultisig() throws InterruptedException { log.info("n {}, k {}", n_k.getLeft(), n_k.getRight()); // You can include up to 3 destinations - Cell msg1 = contract.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.5), 3); - Cell msg2 = contract.createOneInternalMsg(Address.of("EQDUna0j-TKlMU9pOBBHNoLzpwlewHl7S1qXtnaYdTTs_Ict"), Utils.toNano(0.6), 3); - Cell msg3 = contract.createOneInternalMsg(Address.of("EQCAy2ue54I-uDvEgD3qXdqjtrJI4F4OeFn3V10Kgt0jXpQn"), Utils.toNano(0.7), 3); + Cell msg1 = MultisigWallet.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.5), 3); + Cell msg2 = MultisigWallet.createOneInternalMsg(Address.of("EQDUna0j-TKlMU9pOBBHNoLzpwlewHl7S1qXtnaYdTTs_Ict"), Utils.toNano(0.6), 3); + Cell msg3 = MultisigWallet.createOneInternalMsg(Address.of("EQCAy2ue54I-uDvEgD3qXdqjtrJI4F4OeFn3V10Kgt0jXpQn"), Utils.toNano(0.7), 3); // Having message(s) to send you can group it to a new order - Cell order = contract.createOrder(msg1, msg2, msg3); + Cell order = MultisigWallet.createOrder(walletId, queryId, msg1, msg2, msg3); + order.toFile("order.boc", false); + + byte[] orderSignatureUser1 = MultisigWallet.signOrder(ownerKeyPair, order); + byte[] orderSignatureUser2 = MultisigWallet.signOrder(keyPair2, order); + byte[] orderSignatureUser3 = MultisigWallet.signOrder(keyPair3, order); + byte[] orderSignatureUser4 = MultisigWallet.signOrder(keyPair4, order); + byte[] orderSignatureUser5 = MultisigWallet.signOrder(keyPair5, order); + + // collected two more signatures + Cell signedOrder = MultisigWallet.addSignatures(order, + List.of( + MultisigSignature.builder() + .pubKeyPosition(pubkey3Index) + .signature(orderSignatureUser3) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey4Index) + .signature(orderSignatureUser4) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey5Index) + .signature(orderSignatureUser5) + .build() + ) + ); - byte[] orderSignature1 = contract.signCell(ownerKeyPair, order); - byte[] orderSignature2 = contract.signCell(keyPair2, order); - byte[] orderSignature3 = contract.signCell(keyPair3, order); - byte[] orderSignature4 = contract.signCell(keyPair4, order); - byte[] orderSignature5 = contract.signCell(keyPair5, order); + signedOrder.toFile("signedOrder.boc", false); - // send order (request for transaction protected with k of n pubkeys) signed by first owner - contract.sendOrder(tonlib, ownerKeyPair, rootIndex, order, queryId, List.of(orderSignature1)); - Utils.sleep(15, "processing 1st query"); + // submitter keypair must come from User3 or User4, otherwise you get error 34 + contract.sendOrder(tonlib, keyPair5, pubkey5Index, signedOrder); + Utils.sleep(20, "processing 1st query"); - showMessagesInfo(contract.getMessagesUnsigned(tonlib), k, "MessagesUnsigned"); - //MessagesUnsigned query-id 9223372036854775807, creator-i 0, cnt 2, cnt-bits 0, msg 00010818181C_ + Pair queryState = contract.getQueryState(tonlib, queryId); + log.info("get_query_state (query {}): status {}, mask {}", queryId, queryState.getLeft(), queryState.getRight()); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), k, "MessagesSignedByIndex-" + rootIndex); - //MessagesSignedByIndex-0 query-id 9223372036854775807, creator-i 0, cnt 2, cnt-bits 0, msg 00010818181C_ + assertThat(queryState.getLeft()).isEqualTo(-1); + } - showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, rootIndex), k, "MessagesUnsignedByIndex-" + rootIndex); - //MessagesUnsignedByIndex-0 result is empty + /** + * One user deploys a multisig wallet and send the first order, + * other user then collects offline more signatures and sends them to the wallet. + * Hybrid: On-chain/Off-chain consensus. + */ + @Test + public void testWalletMultisigHybrid() throws InterruptedException { - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, pubkey2Index), k, "MessagesSignedByIndex-" + pubkey2Index); - //MessagesSignedByIndex-1 result is empty + Tonlib tonlib = Tonlib.builder().testnet(true).build(); - showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, pubkey2Index), k, "MessagesUnsignedByIndex-" + pubkey2Index); - //MessagesUnsignedByIndex-1 query-id 9223372036854775807, creator-i 0, cnt 2, cnt-bits 0, msg 00010818181C_ + log.info("pubKey0 {}", Utils.bytesToHex(ownerKeyPair.getPublicKey())); + log.info("pubKey2 {}", Utils.bytesToHex(keyPair2.getPublicKey())); + log.info("pubKey3 {}", Utils.bytesToHex(keyPair3.getPublicKey())); + log.info("pubKey4 {}", Utils.bytesToHex(keyPair4.getPublicKey())); + log.info("pubKey5 {}", Utils.bytesToHex(keyPair5.getPublicKey())); - Pair queryState = contract.getQueryState(tonlib, queryId); - log.info("get_query_state (query {}): status {}, mask {}", queryId.toString(10), queryState.getLeft(), queryState.getRight()); - // get_query_state (query 9223372036854775807): status 0 cnt_bits? 1 + BigInteger queryId = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 2 * 60 * 60L, 32)); - // 1 2 3 - Cell query = contract.createQuery(ownerKeyPair, List.of(orderSignature1, orderSignature2, orderSignature3), order); - Pair cnt_mask = contract.checkQuerySignatures(tonlib, query); - log.info("cnt {}, mask {}", cnt_mask.getLeft(), cnt_mask.getRight()); + Long walletId = new Random().nextLong() & 0xffffffffL; + log.info("queryId {}, walletId {}", queryId, walletId); - // 1 2 3 5 - query = contract.createQuery(ownerKeyPair, List.of(orderSignature1, orderSignature2, orderSignature3, orderSignature4, orderSignature5), order); - cnt_mask = contract.checkQuerySignatures(tonlib, query); - log.info("cnt {}, mask {}", cnt_mask.getLeft(), cnt_mask.getRight()); + int rootIndex = 0; + int pubkey2Index = 1; + int pubkey3Index = 2; + int pubkey4Index = 3; + int pubkey5Index = 4; + int k = 3; + int n = 5; - // send order (request for transaction protected with k of n pubkeys) signed by third owner - contract.sendOrder(tonlib, keyPair3, pubkey3Index, order, queryId, List.of(orderSignature3)); - Utils.sleep(15, "processing 2nd query"); + Options options = Options.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .walletId(walletId) + .multisigConfig(MultisigConfig.builder() + .queryId(queryId) + .k(k) + .n(n) + .rootI(rootIndex) + .owners( + List.of( + OwnerInfo.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .flood(1) + .build(), + OwnerInfo.builder() + .publicKey(keyPair2.getPublicKey()) + .flood(2) + .build(), + OwnerInfo.builder() + .publicKey(keyPair3.getPublicKey()) + .flood(3) + .build(), + OwnerInfo.builder() + .publicKey(keyPair4.getPublicKey()) + .flood(4) + .build(), + OwnerInfo.builder() + .publicKey(keyPair5.getPublicKey()) + .flood(5) + .build() + ) + ).build()) + .build(); - showMessagesInfo(contract.getMessagesUnsigned(tonlib), k, "MessagesUnsigned"); - //MessagesUnsigned query-id 9223372036854775807, creator-i 0, cnt 4, cnt-bits 1, msg 00021818181C_ + Wallet wallet = new Wallet(WalletVersion.multisig, options); + MultisigWallet contract = wallet.create(); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), k, "MessagesSignedByIndex-" + rootIndex); - //MessagesSignedByIndex-0 query-id 9223372036854775807, creator-i 0, cnt 4, cnt-bits 1, msg 00021818181C_ + String nonBounceableAddress = contract.getAddress().toString(true, true, false); + String bounceableAddress = contract.getAddress().toString(true, true, true); - showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, rootIndex), k, "MessagesUnsignedByIndex-" + rootIndex); - //MessagesUnsignedByIndex-0 result is empty + log.info("non-bounceable address {}", nonBounceableAddress); + log.info(" bounceable address {}", bounceableAddress); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, pubkey2Index), k, "MessagesSignedByIndex-" + pubkey2Index); - //MessagesSignedByIndex-1 query-id 9223372036854775807, creator-i 0, cnt 4, cnt-bits 1, msg 00021818181C_ + // top up new wallet using test-faucet-wallet + BigInteger balance = TestFaucet.topUpContract(tonlib, Address.of(nonBounceableAddress), Utils.toNano(5)); + Utils.sleep(10, "topping up..."); + log.info("new wallet {} balance: {}", contract.getName(), Utils.formatNanoValue(balance)); - showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, pubkey2Index), k, "MessagesUnsignedByIndex-" + pubkey2Index); - //MessagesUnsignedByIndex-1 result is empty + contract.deploy(tonlib, ownerKeyPair.getSecretKey()); - queryState = contract.getQueryState(tonlib, queryId); - log.info("get_query_state (query {}): status {}, mask {}", queryId.toString(10), queryState.getLeft(), queryState.getRight()); - // get_query_state (query 9223372036854775807): status 0 cnt_bits? 5 + Utils.sleep(30, "deploying"); // with empty ext-msg - // send order (request for transaction protected with k of n pubkeys) signed by fifth owner - contract.sendOrder(tonlib, keyPair5, pubkey5Index, order, queryId, List.of(orderSignature5)); - Utils.sleep(20, "processing 3nd query"); + log.info("owners publicKeysHex {}", contract.getPublicKeysHex(tonlib)); + + Pair n_k = contract.getNandK(tonlib); + log.info("n {}, k {}", n_k.getLeft(), n_k.getRight()); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), k, "MessagesSignedByIndex-" + rootIndex); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), k, "MessagesSignedByIndex-" + pubkey2Index); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), k, "MessagesSignedByIndex-" + pubkey3Index); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), k, "MessagesSignedByIndex-" + pubkey4Index); - showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), k, "MessagesSignedByIndex-" + pubkey5Index); + // You can include up to 3 destinations + Cell msg1 = MultisigWallet.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.5), 3); + Cell msg2 = MultisigWallet.createOneInternalMsg(Address.of("EQDUna0j-TKlMU9pOBBHNoLzpwlewHl7S1qXtnaYdTTs_Ict"), Utils.toNano(0.6), 3); + Cell msg3 = MultisigWallet.createOneInternalMsg(Address.of("EQCAy2ue54I-uDvEgD3qXdqjtrJI4F4OeFn3V10Kgt0jXpQn"), Utils.toNano(0.7), 3); + + // Having message(s) to send you can group it to a new order + Cell order = MultisigWallet.createOrder(walletId, queryId, msg1, msg2, msg3); + order.toFile("order.boc", false); + + byte[] orderSignatureUser1 = MultisigWallet.signOrder(ownerKeyPair, order); + byte[] orderSignatureUser2 = MultisigWallet.signOrder(keyPair2, order); + byte[] orderSignatureUser3 = MultisigWallet.signOrder(keyPair3, order); + byte[] orderSignatureUser4 = MultisigWallet.signOrder(keyPair4, order); + byte[] orderSignatureUser5 = MultisigWallet.signOrder(keyPair5, order); + + contract.sendOrder(tonlib, ownerKeyPair, rootIndex, order); + Utils.sleep(20, "processing 1st query"); + + Pair queryState = contract.getQueryState(tonlib, queryId); + log.info("get_query_state (query {}): status {}, mask {}", queryId, queryState.getLeft(), queryState.getRight()); + + // collected two more signatures + Cell signedOrder = MultisigWallet.addSignatures(order, + List.of( + MultisigSignature.builder() + .pubKeyPosition(pubkey3Index) + .signature(orderSignatureUser3) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey4Index) + .signature(orderSignatureUser4) + .build() + ) + ); + + signedOrder.toFile("signedOrder.boc", false); + + // submitter keypair must come from User3 or User4, otherwise you get error 34 + contract.sendOrder(tonlib, keyPair3, pubkey3Index, signedOrder); + Utils.sleep(20, "processing 1st query"); + + showMessagesInfo(contract.getMessagesUnsigned(tonlib), "Messages-Unsigned"); + showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), "Messages-SignedByIndex-" + rootIndex); + showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, rootIndex), "Messages-UnsignedByIndex-" + rootIndex); + showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, pubkey2Index), "Messages-SignedByIndex-" + pubkey2Index); + showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, pubkey2Index), "Messages-UnsignedByIndex-" + pubkey2Index); queryState = contract.getQueryState(tonlib, queryId); - log.info("get_query_state (query {}): status {}, mask {}", queryId.toString(10), queryState.getLeft(), queryState.getRight()); - // get_query_state (query 9223372036854775807): status 0 cnt_bits? 21 + log.info("get_query_state (query {}): status {}, mask {}", queryId, queryState.getLeft(), queryState.getRight()); + + assertThat(queryState.getLeft()).isEqualTo(-1); - log.info("processed query? ({}) - {}", queryId, contract.processed(tonlib, queryId)); + // 1 2 3 + Cell query = MultisigWallet.createQuery(ownerKeyPair, + List.of( + MultisigSignature.builder() + .pubKeyPosition(rootIndex) + .signature(orderSignatureUser1) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey2Index) + .signature(orderSignatureUser2) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey3Index) + .signature(orderSignatureUser3) + .build() + ), order); + Pair cnt_mask = contract.checkQuerySignatures(tonlib, query); + log.info("cnt {}, mask {}", cnt_mask.getLeft(), cnt_mask.getRight()); + + // 1 2 3 5 + query = MultisigWallet.createQuery(ownerKeyPair, + List.of( + MultisigSignature.builder() + .pubKeyPosition(rootIndex) + .signature(orderSignatureUser1) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey2Index) + .signature(orderSignatureUser2) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey3Index) + .signature(orderSignatureUser3) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey4Index) + .signature(orderSignatureUser4) + .build(), + MultisigSignature.builder() + .pubKeyPosition(pubkey5Index) + .signature(orderSignatureUser5) + .build() + ), order); + + cnt_mask = contract.checkQuerySignatures(tonlib, query); + log.info("cnt {}, mask {}", cnt_mask.getLeft(), cnt_mask.getRight()); } @Test @@ -285,8 +427,11 @@ public void testGetInitState() throws InterruptedException { log.info("state-init {}", stateInit.toHex(false)); } + /** + * Test different root index and multiple orders. Consensus gets calculated on-chain. + */ @Test - public void testRootIAndMultipleOrders() throws InterruptedException { + public void testRootIAndMultipleOrdersOnChain() throws InterruptedException { Tonlib tonlib = Tonlib.builder() .testnet(true) @@ -332,7 +477,6 @@ public void testRootIAndMultipleOrders() throws InterruptedException { .build()) .build(); - Wallet wallet = new Wallet(WalletVersion.multisig, options); MultisigWallet contract = wallet.create(); @@ -356,39 +500,352 @@ public void testRootIAndMultipleOrders() throws InterruptedException { Pair n_k = contract.getNandK(tonlib); log.info("n {}, k {}", n_k.getLeft(), n_k.getRight()); - Cell txMsg1 = contract.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.5), 3); - Cell order1 = contract.createOrder(txMsg1); - byte[] order1Signature3 = contract.signCell(keyPair3, order1); + Cell txMsg1 = MultisigWallet.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.5), 3); + Cell order1 = MultisigWallet.createOrder(walletId, queryId1, txMsg1); + byte[] order1Signature3 = MultisigWallet.signCell(keyPair3, order1); // send order-1 signed by 3rd owner (root index 2) - contract.sendOrder(tonlib, keyPair3, 2, order1, queryId1, List.of(order1Signature3)); // root index 2 + contract.sendOrder(tonlib, keyPair3, 2, order1); // root index 2 Utils.sleep(15, "processing 1st query"); Pair queryState = contract.getQueryState(tonlib, queryId1); log.info("get_query_state (query {}): status {}, mask {}", queryId1.toString(10), queryState.getLeft(), queryState.getRight()); - Cell txMsg2 = contract.createOneInternalMsg(Address.of("EQCAy2ue54I-uDvEgD3qXdqjtrJI4F4OeFn3V10Kgt0jXpQn"), Utils.toNano(0.8), 3); - Cell order2 = contract.createOrder(txMsg2); - byte[] order2Signature2 = contract.signCell(keyPair2, order2); + Cell msg2 = MultisigWallet.createOneInternalMsg(Address.of("EQCAy2ue54I-uDvEgD3qXdqjtrJI4F4OeFn3V10Kgt0jXpQn"), Utils.toNano(0.8), 3); + Cell order2 = MultisigWallet.createOrder(walletId, queryId2, msg2); // send order-2 signed by 2nd owner (root index 1) - contract.sendOrder(tonlib, keyPair2, 1, order2, queryId2, List.of(order2Signature2)); // root index 1 + contract.sendOrder(tonlib, keyPair2, 1, order2); Utils.sleep(15, "processing 2nd query"); queryState = contract.getQueryState(tonlib, queryId2); log.info("get_query_state (query {}): status {}, mask {}", queryId2, queryState.getLeft(), queryState.getRight()); - // send 2nd signature to the 2nd order and thus execute it - byte[] order2Signature3 = contract.signCell(keyPair3, order2); // send order-2 signed by 3rd owner (root index 2) - contract.sendOrder(tonlib, keyPair3, 2, order2, queryId2, List.of(order2Signature3)); // root index 1 + contract.sendOrder(tonlib, keyPair3, 2, order2); Utils.sleep(15, "processing 3rd query"); queryState = contract.getQueryState(tonlib, queryId2); log.info("get_query_state (query {}): status {}, mask {}", queryId2, queryState.getLeft(), queryState.getRight()); + assertThat(queryState.getLeft()).isEqualTo(-1); } - private void showMessagesInfo(Map messages, int k, String label) { + /** + * Each owner sends the extmsg signed by him, containing the order. Consensus gets calculated on-chain. + */ + @Test + public void testEmptySignaturesListOnChain() throws InterruptedException { + + Tonlib tonlib = Tonlib.builder() + .testnet(true) + .build(); + + log.info("pubKey0 {}", Utils.bytesToHex(ownerKeyPair.getPublicKey())); + log.info("pubKey1 {}", Utils.bytesToHex(keyPair2.getPublicKey())); + log.info("pubKey2 {}", Utils.bytesToHex(keyPair3.getPublicKey())); + + BigInteger queryId = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 10 * 60L, 32)); + + Long walletId = new Random().nextLong() & 0xffffffffL; + log.info("queryId-1 {}, walletId {}", queryId.toString(10), walletId); + + int k = 2; + int n = 3; + + Options options = Options.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .walletId(walletId) + .multisigConfig(MultisigConfig.builder() + .queryId(queryId) + .k(k) + .n(n) + .rootI(0) // initial root index + .owners(List.of( + OwnerInfo.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .flood(1) + .build(), + OwnerInfo.builder() + .publicKey(keyPair2.getPublicKey()) + .flood(2) + .build() + )) + .build()) + .build(); + + + Wallet wallet = new Wallet(WalletVersion.multisig, options); + MultisigWallet contract = wallet.create(); + + String nonBounceableAddress = contract.getAddress().toString(true, true, false); + String bounceableAddress = contract.getAddress().toString(true, true, true); + + log.info("non-bounceable address {}", nonBounceableAddress); + log.info(" bounceable address {}", bounceableAddress); + + // top up new wallet using test-faucet-wallet + BigInteger balance = TestFaucet.topUpContract(tonlib, Address.of(nonBounceableAddress), Utils.toNano(1)); + Utils.sleep(10, "topping up..."); + log.info("new wallet {} balance: {}", contract.getName(), Utils.formatNanoValue(balance)); + + contract.deploy(tonlib, ownerKeyPair.getSecretKey()); + + Utils.sleep(30, "deploying"); // with empty ext msg + + log.info("owners publicKeysHex {}", contract.getPublicKeysHex(tonlib)); + + Pair n_k = contract.getNandK(tonlib); + log.info("n {}, k {}", n_k.getLeft(), n_k.getRight()); + + Cell txMsg1 = MultisigWallet.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.5), 3); + Cell order = MultisigWallet.createOrder(walletId, queryId, txMsg1); + + // send order-1 signed by 1st owner (root index 0) + contract.sendOrder(tonlib, ownerKeyPair.getSecretKey(), 0, order); // root index 0 + Utils.sleep(20, "processing 1st query"); + + showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, 0), "MessagesUnsignedByIndex-" + 0); + showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, 0), "MessagesSignedByIndex-" + 0); + + Pair queryState = contract.getQueryState(tonlib, queryId); + log.info("get_query_state (query {}): status {}, mask {}", queryId, queryState.getLeft(), queryState.getRight()); + + // send order-1 signed by 2nd owner (root index 1) + contract.sendOrder(tonlib, keyPair2.getSecretKey(), 1, order); // root index 1 + Utils.sleep(15, "processing 2st query"); + + queryState = contract.getQueryState(tonlib, queryId); + log.info("get_query_state (query {}): status {}, mask {}", queryId, queryState.getLeft(), queryState.getRight()); + assertThat(queryState.getLeft()).isEqualTo(-1); + } + + @Test + public void testMultisigPendingQueries() throws InterruptedException { + + Tonlib tonlib = Tonlib.builder().testnet(true).build(); + + log.info("pubKey0 {}", Utils.bytesToHex(ownerKeyPair.getPublicKey())); + log.info("pubKey2 {}", Utils.bytesToHex(keyPair2.getPublicKey())); + log.info("pubKey3 {}", Utils.bytesToHex(keyPair3.getPublicKey())); + log.info("pubKey4 {}", Utils.bytesToHex(keyPair4.getPublicKey())); + log.info("pubKey5 {}", Utils.bytesToHex(keyPair5.getPublicKey())); + + BigInteger queryId1 = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 5 * 60L, 32)); + BigInteger queryId2 = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 10 * 60L, 32) - 5); + + Long walletId = new Random().nextLong() & 0xffffffffL; + log.info("queryId-1 {}, walletId {}", queryId1.toString(10), walletId); + log.info("queryId-2 {}, walletId {}", queryId2.toString(10), walletId); + + int rootIndex = 0; + int pubkey3Index = 2; + int k = 3; + int n = 5; + + Cell msg1 = MultisigWallet.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.3), 3); + Cell order1 = MultisigWallet.createOrder(walletId, queryId1, msg1); + + Cell msg2 = MultisigWallet.createOneInternalMsg(Address.of("EQDUna0j-TKlMU9pOBBHNoLzpwlewHl7S1qXtnaYdTTs_Ict"), Utils.toNano(0.4), 3); + + Options options = Options.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .walletId(walletId) + .multisigConfig(MultisigConfig.builder() + .queryId(queryId1) + .k(k) + .n(n) + .rootI(rootIndex) + .owners(List.of( + OwnerInfo.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .flood(1) + .build(), + OwnerInfo.builder() + .publicKey(keyPair2.getPublicKey()) + .flood(2) + .build(), + OwnerInfo.builder() + .publicKey(keyPair3.getPublicKey()) + .flood(3) + .build(), + OwnerInfo.builder() + .publicKey(keyPair4.getPublicKey()) + .flood(4) + .build(), + OwnerInfo.builder() + .publicKey(keyPair5.getPublicKey()) + .flood(5) + .build() + )) + .pendingQueries(List.of( + PendingQuery.builder() + .queryId(queryId1) + .creatorI(rootIndex) + .cnt(2) // number of confirmation + .cntBits(3) // bit mask of confirmed pubkeys + .msg(msg1) + .build(), + PendingQuery.builder() + .queryId(queryId2) + .creatorI(rootIndex) + .cnt(1) + .cntBits(1) + .msg(msg2) + .build() + )) + .build()) + .build(); + + Wallet wallet = new Wallet(WalletVersion.multisig, options); + MultisigWallet contract = wallet.create(); + + + String nonBounceableAddress = contract.getAddress().toString(true, true, false); + String bounceableAddress = contract.getAddress().toString(true, true, true); + + log.info("non-bounceable address {}", nonBounceableAddress); + log.info(" bounceable address {}", bounceableAddress); + + // top up new wallet using test-faucet-wallet + BigInteger balance = TestFaucet.topUpContract(tonlib, Address.of(nonBounceableAddress), Utils.toNano(1)); + Utils.sleep(10, "topping up..."); + log.info("new wallet {} balance: {}", contract.getName(), Utils.formatNanoValue(balance)); + + contract.deploy(tonlib, ownerKeyPair.getSecretKey()); + + Utils.sleep(30, "deploying"); // with empty ext msg + + log.info("owners publicKeysHex {}", contract.getPublicKeysHex(tonlib)); + + Pair queryState = contract.getQueryState(tonlib, queryId1); + log.info("get_query_state (query-1 {}): status {}, mask {}", queryId1, queryState.getLeft(), queryState.getRight()); + + queryState = contract.getQueryState(tonlib, queryId2); + log.info("get_query_state (query-2 {}): status {}, mask {}", queryId2, queryState.getLeft(), queryState.getRight()); + + + showMessagesInfo(contract.getMessagesUnsigned(tonlib), "Messages-Unsigned"); + showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), "Messages-SignedByIndex-" + rootIndex); + showMessagesInfo(contract.getMessagesUnsignedByIndex(tonlib, rootIndex), "Messages-UnsignedByIndex-" + rootIndex); + + contract.sendOrder(tonlib, keyPair3.getSecretKey(), pubkey3Index, order1); + + Utils.sleep(20, "processing query"); + + queryState = contract.getQueryState(tonlib, queryId1); + log.info("get_query_state (query-1 {}): status {}, mask {}", queryId1, queryState.getLeft(), queryState.getRight()); + + assertThat(queryState.getLeft()).isEqualTo(-1); + + showMessagesInfo(contract.getMessagesUnsigned(tonlib), "Messages-Unsigned"); + showMessagesInfo(contract.getMessagesSignedByIndex(tonlib, rootIndex), "Messages-SignedByIndex-" + rootIndex); + } + + @Test + public void testMergePendingQueries() throws InterruptedException { + + Tonlib tonlib = Tonlib.builder() + .testnet(true) + .build(); + + log.info("pubKey0 {}", Utils.bytesToHex(ownerKeyPair.getPublicKey())); + log.info("pubKey1 {}", Utils.bytesToHex(keyPair2.getPublicKey())); + log.info("pubKey2 {}", Utils.bytesToHex(keyPair3.getPublicKey())); + + BigInteger queryId1 = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 10 * 60L, 32)); + BigInteger queryId2 = BigInteger.valueOf((long) Math.pow(Instant.now().getEpochSecond() + 10 * 60L, 32) - 5); + + Long walletId = new Random().nextLong() & 0xffffffffL; + log.info("queryId-1 {}, walletId {}", queryId1.toString(10), walletId); + + int k = 2; + int n = 3; + + Cell msg1 = MultisigWallet.createOneInternalMsg(Address.of("EQAaGHUHfkpWFGs428ETmym4vbvRNxCA1o4sTkwqigKjgf-_"), Utils.toNano(0.3), 3); + Cell msg2 = MultisigWallet.createOneInternalMsg(Address.of("EQDUna0j-TKlMU9pOBBHNoLzpwlewHl7S1qXtnaYdTTs_Ict"), Utils.toNano(0.4), 3); + + Options options = Options.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .walletId(walletId) + .multisigConfig(MultisigConfig.builder() + .queryId(queryId1) + .k(k) + .n(n) + .rootI(0) // initial root index + .owners(List.of( + OwnerInfo.builder() + .publicKey(ownerKeyPair.getPublicKey()) + .flood(1) + .build(), + OwnerInfo.builder() + .publicKey(keyPair2.getPublicKey()) + .flood(2) + .build() + )) + .build()) + .build(); + + + Wallet wallet = new Wallet(WalletVersion.multisig, options); + MultisigWallet contract = wallet.create(); + + String nonBounceableAddress = contract.getAddress().toString(true, true, false); + String bounceableAddress = contract.getAddress().toString(true, true, true); + + log.info("non-bounceable address {}", nonBounceableAddress); + log.info(" bounceable address {}", bounceableAddress); + + // top up new wallet using test-faucet-wallet + BigInteger balance = TestFaucet.topUpContract(tonlib, Address.of(nonBounceableAddress), Utils.toNano(1)); + Utils.sleep(10, "topping up..."); + log.info("new wallet {} balance: {}", contract.getName(), Utils.formatNanoValue(balance)); + + contract.deploy(tonlib, ownerKeyPair.getSecretKey()); + + Utils.sleep(30, "deploying"); // with empty ext msg + + log.info("owners publicKeysHex {}", contract.getPublicKeysHex(tonlib)); + Cell dict1 = MultisigWallet.createPendingQueries( + List.of( + PendingQuery.builder() + .queryId(queryId1) + .creatorI(0) + .cnt(2) // number of confirmation + .cntBits(3) // bit mask of confirmed pubkeys + .msg(msg1) + .build(), + PendingQuery.builder() + .queryId(queryId2) + .creatorI(0) + .cnt(2) + .cntBits(3) + .msg(msg1) + .build() + ), n); + + Cell dict2 = MultisigWallet.createPendingQueries( + List.of( + PendingQuery.builder() + .queryId(queryId1) + .creatorI(0) + .cnt(2) + .cntBits(3) + .msg(msg2) + .build(), + PendingQuery.builder() + .queryId(queryId2) + .creatorI(0) + .cnt(2) + .cntBits(3) + .msg(msg1) + .build() + ), n); + + Cell mergeDict = contract.mergePendingQueries(tonlib, dict1, dict2); + log.info("merged dict {}", mergeDict); + } + + private void showMessagesInfo(Map messages, String label) { if (messages.isEmpty()) { log.info("{} result is empty", label); } @@ -396,16 +853,7 @@ private void showMessagesInfo(Map messages, int k, String labe BigInteger query_id = entry.getKey(); Cell query = entry.getValue(); - CellSlice slice = CellSlice.beginParse(query); - slice.skipBit(); - BigInteger creatorI = slice.loadUint(8); - BigInteger cnt = slice.loadUint(8); - - // shows mask (of length k bits) of signed positions, e.g. 101 - first and third signed - // every query might have different k - BigInteger cnt_bits = slice.loadUint(k); - Cell c = slice.sliceToCell(); - log.info("{} query-id {}, creator-i {}, cnt {}, cnt-bits {}, msg {}", label, query_id, creatorI, cnt, cnt_bits, c); + log.info("{} query-id {}, msg {}", label, query_id, query); } } }