diff --git a/CHANGELOG.md b/CHANGELOG.md index d02abd01..cd92e77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ +# 2.4.0 +* **BREAKING CHANGE**: add parameter `tx_dep_provider: &dyn TransactionDependencyProvider` to `ScriptSigner::sing_tx`. +* Support open transaction. + # 2.3.0 * Update ckb to v0.105.1 * **BREAKING CHANGE**: `get_transaction` rpc now return `TransactionWithStatusResponse` diff --git a/examples/transfer_from_opentx.md b/examples/transfer_from_opentx.md new file mode 100644 index 00000000..b076b231 --- /dev/null +++ b/examples/transfer_from_opentx.md @@ -0,0 +1,250 @@ + +This document is about how to use the transfer_from_opentx example to do open transaction operation. +All the addresses and keys are all in my development local node, you should not use in the production environment. +# Sighash open transaction example +1. Build an opentx address +```bash + ./target/debug/examples/transfer_from_opentx build --receiver ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 + ``` + The output: + ```json +{ + "lock-arg": "0x00b398368a8ed39448f95479c1178ff3fc5e31631810", + "lock-hash": "0x3f54ccaf46b3472b55eaa2e2c0a5cae87575b3de90a81fe60206dd5c0951ffa8", + "mainnet": "ckb1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqv8f7ak", + "testnet": "ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7" +} +``` +2. Transfer capacity to the address +```bash +ckb-cli wallet transfer --from-account 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7 \ + --to-address ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \ + --capacity 99 --skip-check-to-address +# 0x937deeb989bbd7f4bd0273bf2049d7614615dd58a32090b0093f23a692715871 +``` +3. Generate the transaction +```bash +./target/debug/examples/transfer_from_opentx gen-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \ + --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \ + --capacity 98.0 --open-capacity 1.0\ + --tx-file tx.json +``` +4. Sign the transaction +```bash +./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \ + --tx-file tx.json +``` +5. Add input, with capacity 98.99999588 +```bash +./target/debug/examples/transfer_from_opentx add-input --tx-hash df85d2aaa44d50b1db286bdb2fbd8682cad12d6858b269d2531403ba5e63a2eb --index 0 --tx-file tx.json +``` +6. Add output, capacity is 98.99999588(original) + 1(open capacity) - 0.001(fee) +```bash +./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 99.99899588 --tx-file tx.json +``` +7. Sign the new input +```bash +./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json +``` +8. send the tx +```bash +./target/debug/examples/transfer_from_opentx send --tx-file tx.json +# 0xebb9d9ff39efbee5957d6f7d19a4a17f1ac2e69dbc9289e4931cef6b832f4d57 +``` + +# Ethereum open transaction example +1. build an opentx address +```bash +./target/debug/examples/transfer_from_opentx build --ethereum-receiver 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d +``` +output: +```json +{ + "lock-arg": "0x01cf2485c76aff1f2b4464edf04a1c8045068cf7e010", + "lock-hash": "0x057dcd204f26621ef49346ed77d2bdbf3069b83a5ef0a2b52be5299a93507cf6", + "mainnet": "ckb1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgpeujgt3m2lu0jk3ryahcy58yqg5rgealqzqjzc5z5", + "testnet": "ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgpeujgt3m2lu0jk3ryahcy58yqg5rgealqzqf4vcru" +} +``` +2. Transfer capacity to the address +```bash +ckb-cli wallet transfer --from-account 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7 \ + --to-address ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgpeujgt3m2lu0jk3ryahcy58yqg5rgealqzqf4vcru \ + --capacity 99 --skip-check-to-address +# 0xbd696b87629dfe38136c52e579800a432622baf5893b61365c7a18902a9ccd60 +``` +3. Generate the transaction +```bash +./target/debug/examples/transfer_from_opentx gen-open-tx --ethereum-sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \ + --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \ + --capacity 98.0 --open-capacity 1.0\ + --tx-file tx.json +``` +4. Sign the transaction +```bash +./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \ + --tx-file tx.json +``` +5. Add input, with capacity 99.99899588 +```bash +./target/debug/examples/transfer_from_opentx add-input --tx-hash ebb9d9ff39efbee5957d6f7d19a4a17f1ac2e69dbc9289e4931cef6b832f4d57 --index 1 --tx-file tx.json +``` +6. Add output, capacity is 99.99899588(original) + 1(open capacity) - 0.001(fee) +```bash +./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 100.99799588 --tx-file tx.json +``` +7. Sighash sign the new input +```bash +./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json +``` +8. Send the transaction +```bash +./target/debug/examples/transfer_from_opentx send --tx-file tx.json +# 0x621077216f3bf7861beacd3cdda44f7a5854454fcd133922b89f0addd0330e6b +``` +# Multisig open transaction example +1. build an opentx address +```bash +./target/debug/examples/transfer_from_opentx build --require-first-n 0 \ + --threshold 2 \ + --sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \ + --sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \ + --sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4 +``` +The output: +```json +{ + "lock-arg": "0x065d7d0128eeaa6f9656a229b42aadd0b177d387eb10", + "lock-hash": "0xf5202949800af0b454b2e4806c57da1d0f3ae87f7b9f4b698d9f3b71162ec196", + "mainnet": "ckb1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgxt47sz28w4fhev44z9x6z4twsk9ma8pltzqmtamce", + "testnet": "ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgxt47sz28w4fhev44z9x6z4twsk9ma8pltzqqufhe3" +} +``` +2. Transfer capacity to the address +```bash +ckb-cli wallet transfer --from-account 0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7 \ + --to-address ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgxt47sz28w4fhev44z9x6z4twsk9ma8pltzqqufhe3 \ + --capacity 99 --skip-check-to-address +# 0xf993b27a0129f72ec0a889cb016987c3cef00f7819461e51d5755464da6adf1b +``` +3. Generate the transaction +```bash +./target/debug/examples/transfer_from_opentx gen-open-tx \ + --require-first-n 0 \ + --threshold 2 \ + --sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \ + --sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \ + --sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4 \ + --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \ + --capacity 98.0 --open-capacity 1.0 \ + --tx-file tx.json +``` +4. Sign the transaction, this step can sign seperately with each sender-key +```bash +./target/debug/examples/transfer_from_opentx sign-open-tx \ + --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \ + --sender-key d00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \ + --tx-file tx.json +``` +5. Add input, with capacity 100.99799588 +```bash +./target/debug/examples/transfer_from_opentx add-input --tx-hash 621077216f3bf7861beacd3cdda44f7a5854454fcd133922b89f0addd0330e6b --index 1 --tx-file tx.json +``` +6. Add output, capacity is 100.99799588(original) + 1(open capacity) - 0.001(fee) +```bash +./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 101.99699588 --tx-file tx.json +``` +7. Sighash sign the new input +```bash +./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json +``` +8. Send the tx +```bash +./target/debug/examples/transfer_from_opentx send --tx-file tx.json +# 0x577101b031d709992af99bd0715172bdb4d2eb7be9f11e84d6fb24ac3e1ac675 +``` +# Put multiple open transactions together +1. Build/sign sighash open transaction +```bash +./target/debug/examples/transfer_from_opentx gen-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \ + --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \ + --capacity 97 --open-capacity 1\ + --tx-file tx-sighash.json +./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \ + --tx-file tx-sighash.json +``` +2. Build/sign sighash open transaction +```bash +./target/debug/examples/transfer_from_opentx gen-open-tx --ethereum-sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \ + --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \ + --capacity 97 --open-capacity 1\ + --tx-file tx-ethereum.json +./target/debug/examples/transfer_from_opentx sign-open-tx --sender-key 63d86723e08f0f813a36ce6aa123bb2289d90680ae1e99d4de8cdb334553f24d \ + --tx-file tx-ethereum.json +``` +3. Build/sign multisig open transaction +```bash +./target/debug/examples/transfer_from_opentx gen-open-tx \ + --require-first-n 0 \ + --threshold 2 \ + --sighash-address ckt1qyqt8xpk328d89zgl928nsgh3lelch33vvvq5u3024 \ + --sighash-address ckt1qyqvsv5240xeh85wvnau2eky8pwrhh4jr8ts8vyj37 \ + --sighash-address ckt1qyqywrwdchjyqeysjegpzw38fvandtktdhrs0zaxl4 \ + --receiver ckt1qqwmhmsv9cmqhag4qxguaqux05rc4qlyq393vu45dhxrrycyutcl6qgqkwvrdz5w6w2y372508q30rlnl30rzccczqhsaju7 \ + --capacity 97 --open-capacity 1.0 \ + --tx-file tx-multisig.json +./target/debug/examples/transfer_from_opentx sign-open-tx \ + --sender-key 8dadf1939b89919ca74b58fef41c0d4ec70cd6a7b093a0c8ca5b268f93b8181f \ + --sender-key d00c06bfd800d27397002dca6fb0993d5ba6399b4238b2f29ee9deb97593d2bc \ + --tx-file tx-multisig.json +``` +4. merge into one transaction + +You can merge them in one command: +```bash +./target/debug/examples/transfer_from_opentx merge-open-tx \ + --in-tx-file tx-sighash.json \ + --in-tx-file tx-ethereum.json \ + --in-tx-file tx-multisig.json \ + --tx-file tx.json +``` + The other way get the same merge result: ++ Merge first 2, then merge the last +```bash +./target/debug/examples/transfer_from_opentx merge-open-tx \ + --in-tx-file tx-sighash.json \ + --in-tx-file tx-ethereum.json \ + --tx-file tx.json +./target/debug/examples/transfer_from_opentx merge-open-tx \ + --in-tx-file tx.json \ + --in-tx-file tx-multisig.json \ + --tx-file tx.json +``` ++ Merge last 2, then merge the first +```bash +./target/debug/examples/transfer_from_opentx merge-open-tx \ + --in-tx-file tx-ethereum.json \ + --in-tx-file tx-multisig.json \ + --tx-file tx.json +./target/debug/examples/transfer_from_opentx merge-open-tx \ + --in-tx-file tx-sighash.json \ + --in-tx-file tx.json \ + --tx-file tx.json +``` +5. Add input, with capacity 101.99699588 +```bash +./target/debug/examples/transfer_from_opentx add-input --tx-hash 577101b031d709992af99bd0715172bdb4d2eb7be9f11e84d6fb24ac3e1ac675 --index 1 --tx-file tx.json +``` +6. Add output, capacity is 101.99699588(original) + 3(1 open capacity each) - 0.001(fee) +```bash +./target/debug/examples/transfer_from_opentx add-output --to-address ckt1qyqy68e02pll7qd9m603pqkdr29vw396h6dq50reug --capacity 104.99599588 --tx-file tx.json +``` +7. Sighash sign the new input +```bash +./target/debug/examples/transfer_from_opentx sighash-sign-tx --sender-key 7068b4dc5289353c688e2e67b75207eb5574ba4938091cf5626a4d0f5cc91668 --tx-file tx.json +``` +8. Send the transaction +```bash +./target/debug/examples/transfer_from_opentx send --tx-file tx.json +# 0x4fd5d4adfb009a6e342a9e8442ac54989e28ef887b1fec60c3703e4c4d223b39 +``` \ No newline at end of file diff --git a/examples/transfer_from_opentx.rs b/examples/transfer_from_opentx.rs new file mode 100644 index 00000000..882dee5d --- /dev/null +++ b/examples/transfer_from_opentx.rs @@ -0,0 +1,725 @@ +/* +How to use the example transfer_from_opentx, see the file transfer_from_opentx.md +*/ +use ckb_crypto::secp::Pubkey; +use ckb_hash::blake2b_256; +use ckb_jsonrpc_types as json_types; +use ckb_sdk::{ + constants::SIGHASH_TYPE_HASH, + rpc::CkbRpcClient, + traits::{ + DefaultCellCollector, DefaultCellDepResolver, DefaultHeaderDepResolver, + DefaultTransactionDependencyProvider, SecpCkbRawKeySigner, + }, + tx_builder::{ + balance_tx_capacity, fill_placeholder_witnesses, omni_lock::OmniLockTransferBuilder, + unlock_tx, CapacityBalancer, TxBuilder, + }, + types::NetworkType, + unlock::{ + opentx::{assembler::assemble_new_tx, OpentxWitness}, + IdentityFlag, MultisigConfig, OmniLockConfig, OmniLockScriptSigner, SecpSighashUnlocker, + }, + unlock::{OmniLockUnlocker, OmniUnlockMode, ScriptUnlocker}, + util::{blake160, keccak160}, + Address, HumanCapacity, ScriptGroup, ScriptId, SECP256K1, +}; +use ckb_types::{ + bytes::Bytes, + core::{BlockView, Capacity, ScriptHashType, TransactionView}, + packed::{Byte32, CellDep, CellOutput, OutPoint, Script, Transaction, WitnessArgs}, + prelude::*, + H160, H256, +}; +use clap::{Args, Parser, Subcommand}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, error::Error as StdErr, fs, path::PathBuf}; + +// The transaction hash i deployed my omnilock script with open transaction function on my test environment. +// You should not use this hash to find your open transaction code_hash at any circumstances, +// or you will lost your CKB for ever, or not make the example work. +// You should replace this hash with your own transaction hash or provide one with command line parameter for this example. +const OPENTX_TX_HASH: &str = "cfcc8aa04d963cb40d79eab17e6f8def536defca203b71a8fce9cfb950dd04fc"; +const OPENTX_TX_IDX: &str = "0"; + +#[derive(Args)] +struct MultiSigArgs { + /// Require first n signatures of corresponding pubkey + #[clap(long, value_name = "NUM", default_value = "1")] + require_first_n: u8, + + /// Multisig threshold + #[clap(long, value_name = "NUM", default_value = "1")] + threshold: u8, + + /// Normal sighash address + #[clap(long, value_name = "ADDRESS")] + sighash_address: Vec
, +} +#[derive(Args)] +struct BuildOmniLockAddrArgs { + /// The receiver address + #[clap(long, value_name = "ADDRESS", group = "algorithm")] + receiver: Option
, + + /// The receiver's private key (hex string) + #[clap(long, value_name = "KEY", group = "algorithm")] + ethereum_receiver: Option, + + #[clap(flatten)] + multis_args: MultiSigArgs, + + /// omnilock script deploy transaction hash + #[clap( + long, + value_name = "H256", + default_value = OPENTX_TX_HASH + )] + omnilock_tx_hash: H256, + + /// cell index of omnilock script deploy transaction's outputs + #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)] + omnilock_index: usize, + + /// CKB rpc url + #[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")] + ckb_rpc: String, +} +#[derive(Args)] +struct GenOpenTxArgs { + /// The sender private key (hex string) + #[clap(long, value_name = "KEY")] + sender_key: Option, + /// The sender private key (hex string) + #[clap(long, value_name = "KEY")] + ethereum_sender_key: Option, + + #[clap(flatten)] + multis_args: MultiSigArgs, + + /// The receiver address + #[clap(long, value_name = "ADDRESS")] + receiver: Address, + + /// omnilock script deploy transaction hash + #[clap(long, value_name = "H256", default_value = OPENTX_TX_HASH)] + omnilock_tx_hash: H256, + + /// cell index of omnilock script deploy transaction's outputs + #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)] + omnilock_index: usize, + + /// The capacity to transfer (unit: CKB, example: 102.43) + #[clap(long, value_name = "CKB")] + capacity: HumanCapacity, + /// The open transaction capacity not decided to whom (unit: CKB, example: 102.43) + #[clap(long, value_name = "CKB")] + open_capacity: HumanCapacity, + #[clap(long, value_name = "NUMBER", default_value = "0")] + fee_rate: u64, + /// The output transaction info file (.json) + #[clap(long, value_name = "PATH")] + tx_file: PathBuf, + + /// CKB rpc url + #[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")] + ckb_rpc: String, +} + +#[derive(Args)] +struct SignTxArgs { + /// The sender private key (hex string) + #[clap(long, value_name = "KEY")] + sender_key: Vec, + + /// The output transaction info file (.json) + #[clap(long, value_name = "PATH")] + tx_file: PathBuf, + + /// omnilock script deploy transaction hash + #[clap(long, value_name = "H256", default_value = OPENTX_TX_HASH)] + omnilock_tx_hash: H256, + + /// cell index of omnilock script deploy transaction's outputs + #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)] + omnilock_index: usize, + + /// CKB rpc url + #[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")] + ckb_rpc: String, +} + +#[derive(Args)] +struct AddInputArgs { + /// omnilock script deploy transaction hash + #[clap(long, value_name = "H256")] + tx_hash: H256, + + /// cell index of omnilock script deploy transaction's outputs + #[clap(long, value_name = "NUMBER")] + index: usize, + + /// The output transaction info file (.json) + #[clap(long, value_name = "PATH")] + tx_file: PathBuf, + + /// omnilock script deploy transaction hash + #[clap(long, value_name = "H256", default_value = OPENTX_TX_HASH)] + omnilock_tx_hash: H256, + + /// cell index of omnilock script deploy transaction's outputs + #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)] + omnilock_index: usize, + + /// CKB rpc url + #[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")] + ckb_rpc: String, +} + +#[derive(Args)] +struct AddOutputArgs { + /// --to-sighash-address ckt1qyqg7zchpds6lv3v0nr36z2msu2x9a5lkhrq7kvyww --capacity 19999.9999 --tx-file tx.json + #[clap(long, value_name = "ADDRESS")] + to_address: Address, + /// The capacity to transfer (unit: CKB, example: 102.43) + #[clap(long, value_name = "CKB")] + capacity: HumanCapacity, + + /// The output transaction info file (.json) + #[clap(long, value_name = "PATH")] + tx_file: PathBuf, +} + +#[derive(Args)] +struct MergeOpenTxArgs { + /// The output transaction info file (.json) + #[clap(long, value_name = "PATH")] + in_tx_file: Vec, + + /// The output transaction info file (.json) + #[clap(long, value_name = "PATH")] + tx_file: PathBuf, + /// omnilock script deploy transaction hash + #[clap(long, value_name = "H256", default_value = OPENTX_TX_HASH)] + omnilock_tx_hash: H256, + + /// cell index of omnilock script deploy transaction's outputs + #[clap(long, value_name = "NUMBER", default_value = OPENTX_TX_IDX)] + omnilock_index: usize, + + /// CKB rpc url + #[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")] + ckb_rpc: String, +} + +#[derive(Subcommand)] +enum Commands { + /// build omni lock address + Build(BuildOmniLockAddrArgs), + /// Generate the transaction + GenOpenTx(GenOpenTxArgs), + /// Sign the open transaction + SignOpenTx(SignTxArgs), + /// sign sighash input + SighashSignTx(SignTxArgs), + /// merge opentx together + MergeOpenTx(MergeOpenTxArgs), + /// Add input + AddInput(AddInputArgs), + /// Add output + AddOutput(AddOutputArgs), + /// Send the transaction + Send { + /// The transaction info file (.json) + #[clap(long, value_name = "PATH")] + tx_file: PathBuf, + + /// CKB rpc url + #[clap(long, value_name = "URL", default_value = "http://127.0.0.1:8114")] + ckb_rpc: String, + }, +} +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Serialize, Deserialize)] +struct TxInfo { + tx: json_types::TransactionView, + omnilock_config: OmniLockConfig, +} + +struct OmniLockInfo { + type_hash: H256, + script_id: ScriptId, + cell_dep: CellDep, +} + +fn main() -> Result<(), Box> { + // Parse arguments + let cli = Cli::parse(); + match cli.command { + Commands::Build(build_args) => build_omnilock_addr(&build_args)?, + Commands::GenOpenTx(gen_args) => { + gen_open_tx(&gen_args)?; + } + Commands::SignOpenTx(args) => { + let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?; + let tx = Transaction::from(tx_info.tx.inner).into_view(); + let keys: Vec = args + .sender_key + .iter() + .map(|sender_key| { + secp256k1::SecretKey::from_slice(sender_key.as_bytes()) + .map_err(|err| format!("invalid sender secret key: {}", err)) + .unwrap() + }) + .collect(); + if tx_info.omnilock_config.is_pubkey_hash() || tx_info.omnilock_config.is_ethereum() { + for (i, key) in keys.iter().enumerate() { + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, key); + let hash160 = match tx_info.omnilock_config.id().flag() { + IdentityFlag::PubkeyHash => { + blake2b_256(&pubkey.serialize()[..])[0..20].to_vec() + } + IdentityFlag::Ethereum => { + keccak160(Pubkey::from(pubkey).as_ref()).as_bytes().to_vec() + } + _ => unreachable!(), + }; + if tx_info.omnilock_config.id().auth_content().as_bytes() != hash160 { + return Err(format!( + "key {:#x} is not in omnilock config", + args.sender_key[i] + ) + .into()); + } + } + } + let (tx, _) = sign_tx(&args, tx, &tx_info.omnilock_config, keys)?; + let witness_args = + WitnessArgs::from_slice(tx.witnesses().get(0).unwrap().raw_data().as_ref())?; + let lock_field = witness_args.lock().to_opt().unwrap().raw_data(); + if lock_field != tx_info.omnilock_config.zero_lock(OmniUnlockMode::Normal)? { + println!("> transaction has been signed!"); + } else { + println!("failed to sign tx"); + } + let tx_info = TxInfo { + tx: json_types::TransactionView::from(tx), + omnilock_config: tx_info.omnilock_config, + }; + fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?; + } + Commands::SighashSignTx(args) => { + let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?; + let tx = Transaction::from(tx_info.tx.inner).into_view(); + let (tx, _) = sighash_sign(&args, tx)?; + let witness_args = + WitnessArgs::from_slice(tx.witnesses().get(0).unwrap().raw_data().as_ref())?; + let lock_field = witness_args.lock().to_opt().unwrap().raw_data(); + if lock_field != tx_info.omnilock_config.zero_lock(OmniUnlockMode::Normal)? { + println!("> transaction ready to send!"); + } else { + println!("failed to sign tx"); + } + let tx_info = TxInfo { + tx: json_types::TransactionView::from(tx), + omnilock_config: tx_info.omnilock_config, + }; + fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?; + } + Commands::AddInput(args) => { + let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?; + // println!("> tx: {}", serde_json::to_string_pretty(&tx_info.tx)?); + let tx = Transaction::from(tx_info.tx.inner).into_view(); + let tx = add_live_cell(&args, tx)?; + let tx_info = TxInfo { + tx: json_types::TransactionView::from(tx), + omnilock_config: tx_info.omnilock_config, + }; + fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?; + } + Commands::AddOutput(args) => { + let tx_info: TxInfo = serde_json::from_slice(&fs::read(&args.tx_file)?)?; + // println!("> tx: {}", serde_json::to_string_pretty(&tx_info.tx)?); + let tx = Transaction::from(tx_info.tx.inner).into_view(); + let lock_script = Script::from(args.to_address.payload()); + let output = CellOutput::new_builder() + .capacity(Capacity::shannons(args.capacity.0).pack()) + .lock(lock_script) + .build(); + let tx = tx + .as_advanced_builder() + .output(output) + .output_data(Bytes::default().pack()) + .build(); + let tx_info = TxInfo { + tx: json_types::TransactionView::from(tx), + omnilock_config: tx_info.omnilock_config, + }; + fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?; + } + Commands::Send { tx_file, ckb_rpc } => { + // Send transaction + let tx_info: TxInfo = serde_json::from_slice(&fs::read(&tx_file)?)?; + println!("> tx: {}", serde_json::to_string_pretty(&tx_info.tx)?); + let outputs_validator = Some(json_types::OutputsValidator::Passthrough); + let _tx_hash = CkbRpcClient::new(ckb_rpc.as_str()) + .send_transaction(tx_info.tx.inner, outputs_validator) + .expect("send transaction"); + println!(">>> tx sent! <<<"); + } + Commands::MergeOpenTx(args) => { + let mut txes = vec![]; + let mut omnilock_config = None; + for in_tx in &args.in_tx_file { + let tx_info: TxInfo = serde_json::from_slice(&fs::read(in_tx)?)?; + // println!("> tx: {}", serde_json::to_string_pretty(&tx_info.tx)?); + let tx = Transaction::from(tx_info.tx.inner).into_view(); + txes.push(tx); + omnilock_config = Some(tx_info.omnilock_config); + } + if !txes.is_empty() { + let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str()); + let cell = build_omnilock_cell_dep( + &mut ckb_client, + &args.omnilock_tx_hash, + args.omnilock_index, + )?; + let tx_dep_provider = + DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10); + let tx = assemble_new_tx(txes, &tx_dep_provider, cell.type_hash.pack())?; + let tx_info = TxInfo { + tx: json_types::TransactionView::from(tx), + omnilock_config: omnilock_config.unwrap(), + }; + fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?; + } + } + } + + Ok(()) +} + +fn build_multisig_config( + sighash_address: &[Address], + require_first_n: u8, + threshold: u8, +) -> Result> { + if sighash_address.is_empty() { + return Err("Must have at least one sighash_address".to_string().into()); + } + let mut sighash_addresses = Vec::with_capacity(sighash_address.len()); + for addr in sighash_address { + let lock_args = addr.payload().args(); + if addr.payload().code_hash(None).as_slice() != SIGHASH_TYPE_HASH.as_bytes() + || addr.payload().hash_type() != ScriptHashType::Type + || lock_args.len() != 20 + { + return Err(format!("sighash_address {} is not sighash address", addr).into()); + } + sighash_addresses.push(H160::from_slice(lock_args.as_ref()).unwrap()); + } + Ok(MultisigConfig::new_with( + sighash_addresses, + require_first_n, + threshold, + )?) +} + +fn build_omnilock_addr(args: &BuildOmniLockAddrArgs) -> Result<(), Box> { + let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str()); + let cell = + build_omnilock_cell_dep(&mut ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?; + let mut config = if let Some(receiver) = args.receiver.as_ref() { + let arg = H160::from_slice(&receiver.payload().args()).unwrap(); + OmniLockConfig::new_pubkey_hash(arg) + } else if let Some(ethereum_receiver) = args.ethereum_receiver.as_ref() { + let privkey = secp256k1::SecretKey::from_slice(ethereum_receiver.as_bytes()).unwrap(); + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &privkey); + // println!("pubkey:{:?}", hex_string(&pubkey.serialize())); + // println!("pubkey:{:?}", hex_string(&pubkey.serialize_uncompressed())); + let addr = keccak160(Pubkey::from(pubkey).as_ref()); + OmniLockConfig::new_ethereum(addr) + } else if !args.multis_args.sighash_address.is_empty() { + let args = &args.multis_args; + let multisig_config = + build_multisig_config(&args.sighash_address, args.require_first_n, args.threshold)?; + OmniLockConfig::new_multisig(multisig_config) + } else { + return Err("must provide a receiver or an ethereum_receiver".into()); + }; + config.set_opentx_mode(); + let address_payload = { + let args = config.build_args(); + ckb_sdk::AddressPayload::new_full(ScriptHashType::Type, cell.type_hash.pack(), args) + }; + let lock_script = Script::from(&address_payload); + let resp = serde_json::json!({ + "mainnet": Address::new(NetworkType::Mainnet, address_payload.clone(), true).to_string(), + "testnet": Address::new(NetworkType::Testnet, address_payload.clone(), true).to_string(), + "lock-arg": format!("0x{}", hex_string(address_payload.args().as_ref())), + "lock-hash": format!("{:#x}", lock_script.calc_script_hash()) + }); + println!("{}", serde_json::to_string_pretty(&resp)?); + Ok(()) +} + +fn gen_open_tx(args: &GenOpenTxArgs) -> Result<(), Box> { + let (tx, omnilock_config) = build_open_tx(args)?; + let tx_info = TxInfo { + tx: json_types::TransactionView::from(tx), + omnilock_config, + }; + fs::write(&args.tx_file, serde_json::to_string_pretty(&tx_info)?)?; + Ok(()) +} + +fn build_open_tx( + args: &GenOpenTxArgs, +) -> Result<(TransactionView, OmniLockConfig), Box> { + let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str()); + let cell = + build_omnilock_cell_dep(&mut ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?; + + let mut omnilock_config = if let Some(sender_key) = args.sender_key.as_ref() { + let sender_key = secp256k1::SecretKey::from_slice(sender_key.as_bytes()) + .map_err(|err| format!("invalid sender secret key: {}", err))?; + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &sender_key); + let pubkey_hash = blake160(&pubkey.serialize()); + OmniLockConfig::new_pubkey_hash(pubkey_hash) + } else if let Some(sender_key) = args.ethereum_sender_key.as_ref() { + let sender_key = secp256k1::SecretKey::from_slice(sender_key.as_bytes()) + .map_err(|err| format!("invalid sender secret key: {}", err))?; + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &sender_key); + println!("pubkey:{:?}", hex_string(&pubkey.serialize())); + println!("pubkey:{:?}", hex_string(&pubkey.serialize_uncompressed())); + let addr = keccak160(Pubkey::from(pubkey).as_ref()); + OmniLockConfig::new_ethereum(addr) + } else if !args.multis_args.sighash_address.is_empty() { + let args = &args.multis_args; + let multisig_config = + build_multisig_config(&args.sighash_address, args.require_first_n, args.threshold)?; + OmniLockConfig::new_multisig(multisig_config) + } else { + return Err("must provide a sender-key or an ethereum-sender-key".into()); + }; + omnilock_config.set_opentx_mode(); + // Build CapacityBalancer + let sender = Script::new_builder() + .code_hash(cell.type_hash.pack()) + .hash_type(ScriptHashType::Type.into()) + .args(omnilock_config.build_args().pack()) + .build(); + let placeholder_witness = omnilock_config.placeholder_witness(OmniUnlockMode::Normal)?; + let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, args.fee_rate); + + // Build: + // * CellDepResolver + // * HeaderDepResolver + // * CellCollector + // * TransactionDependencyProvider + let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str()); + let genesis_block = ckb_client.get_block_by_number(0.into())?.unwrap(); + let genesis_block = BlockView::from(genesis_block); + let mut cell_dep_resolver = DefaultCellDepResolver::from_genesis(&genesis_block)?; + cell_dep_resolver.insert(cell.script_id, cell.cell_dep, "Omni Lock".to_string()); + let header_dep_resolver = DefaultHeaderDepResolver::new(args.ckb_rpc.as_str()); + let mut cell_collector = DefaultCellCollector::new(args.ckb_rpc.as_str()); + let tx_dep_provider = DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10); + + // Build base transaction + let unlockers = build_omnilock_unlockers(Vec::new(), omnilock_config.clone(), cell.type_hash); + let output = CellOutput::new_builder() + .lock(sender.clone()) + .capacity(args.capacity.0.pack()) + .build(); + + let builder = OmniLockTransferBuilder::new_open( + args.open_capacity, + vec![(output, Bytes::default())], + omnilock_config.clone(), + None, + ); + + let base_tx = builder.build_base( + &mut cell_collector, + &cell_dep_resolver, + &header_dep_resolver, + &tx_dep_provider, + )?; + + let secp256k1_data_dep = { + // pub const SECP256K1_DATA_OUTPUT_LOC: (usize, usize) = (0, 3); + let tx_hash = genesis_block.transactions()[0].hash(); + let out_point = OutPoint::new(tx_hash, 3u32); + CellDep::new_builder().out_point(out_point).build() + }; + + let base_tx = base_tx + .as_advanced_builder() + .cell_dep(secp256k1_data_dep) + .build(); + let (tx, _) = fill_placeholder_witnesses(base_tx, &tx_dep_provider, &unlockers)?; + + let tx = balance_tx_capacity( + &tx, + &balancer, + &mut cell_collector, + &tx_dep_provider, + &cell_dep_resolver, + &header_dep_resolver, + )?; + + let wit = OpentxWitness::new_sig_all_relative(&tx, Some(0xdeadbeef)).unwrap(); + omnilock_config.set_opentx_input(wit); + let tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &omnilock_config, + OmniUnlockMode::Normal, + &tx_dep_provider, + &sender, + )?; + Ok((tx, omnilock_config)) +} + +fn build_omnilock_cell_dep( + ckb_client: &mut CkbRpcClient, + tx_hash: &H256, + index: usize, +) -> Result> { + let out_point_json = ckb_jsonrpc_types::OutPoint { + tx_hash: tx_hash.clone(), + index: ckb_jsonrpc_types::Uint32::from(index as u32), + }; + let cell_status = ckb_client.get_live_cell(out_point_json, false)?; + let script = Script::from(cell_status.cell.unwrap().output.type_.unwrap()); + + let type_hash = script.calc_script_hash(); + let out_point = OutPoint::new(Byte32::from_slice(tx_hash.as_bytes())?, index as u32); + + let cell_dep = CellDep::new_builder().out_point(out_point).build(); + Ok(OmniLockInfo { + type_hash: H256::from_slice(type_hash.as_slice())?, + script_id: ScriptId::new_type(type_hash.unpack()), + cell_dep, + }) +} + +fn add_live_cell( + args: &AddInputArgs, + tx: TransactionView, +) -> Result> { + let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str()); + let out_point_json = ckb_jsonrpc_types::OutPoint { + tx_hash: args.tx_hash.clone(), + index: ckb_jsonrpc_types::Uint32::from(args.index as u32), + }; + let cell_with_status = ckb_client.get_live_cell(out_point_json, false)?; + let input_outpoint = OutPoint::new( + Byte32::from_slice(args.tx_hash.as_bytes())?, + args.index as u32, + ); + // since value should be provided in args + let input = ckb_types::packed::CellInput::new(input_outpoint, 0); + let cell_dep_resolver = { + let genesis_block = ckb_client.get_block_by_number(0.into())?.unwrap(); + DefaultCellDepResolver::from_genesis(&BlockView::from(genesis_block))? + }; + let code_hash = cell_with_status.cell.unwrap().output.lock.code_hash; + let script_id = ScriptId::new_type(code_hash); + let dep = cell_dep_resolver + .get(&script_id) + .as_ref() + .unwrap() + .0 + .clone(); + + Ok(tx.as_advanced_builder().input(input).cell_dep(dep).build()) +} + +fn build_omnilock_unlockers( + keys: Vec, + config: OmniLockConfig, + omni_lock_type_hash: H256, +) -> HashMap> { + let signer = match config.id().flag() { + IdentityFlag::PubkeyHash => SecpCkbRawKeySigner::new_with_secret_keys(keys), + IdentityFlag::Ethereum => SecpCkbRawKeySigner::new_with_ethereum_secret_keys(keys), + IdentityFlag::Multisig => SecpCkbRawKeySigner::new_with_secret_keys(keys), + _ => unreachable!("should not reach here!"), + }; + let omnilock_signer = + OmniLockScriptSigner::new(Box::new(signer), config.clone(), OmniUnlockMode::Normal); + let omnilock_unlocker = OmniLockUnlocker::new(omnilock_signer, config); + let omnilock_script_id = ScriptId::new_type(omni_lock_type_hash); + HashMap::from([( + omnilock_script_id, + Box::new(omnilock_unlocker) as Box, + )]) +} + +fn sign_tx( + args: &SignTxArgs, + mut tx: TransactionView, + omnilock_config: &OmniLockConfig, + keys: Vec, +) -> Result<(TransactionView, Vec), Box> { + // Unlock transaction + let tx_dep_provider = DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10); + + let mut ckb_client = CkbRpcClient::new(args.ckb_rpc.as_str()); + let cell = + build_omnilock_cell_dep(&mut ckb_client, &args.omnilock_tx_hash, args.omnilock_index)?; + + let mut _still_locked_groups = None; + let unlockers = build_omnilock_unlockers(keys, omnilock_config.clone(), cell.type_hash); + let (new_tx, new_still_locked_groups) = unlock_tx(tx.clone(), &tx_dep_provider, &unlockers)?; + tx = new_tx; + _still_locked_groups = Some(new_still_locked_groups); + Ok((tx, _still_locked_groups.unwrap_or_default())) +} + +fn sighash_sign( + args: &SignTxArgs, + tx: TransactionView, +) -> Result<(TransactionView, Vec), Box> { + if args.sender_key.is_empty() { + return Err("must provide sender-key to sign".into()); + } + let sender_key = secp256k1::SecretKey::from_slice(args.sender_key[0].as_bytes()) + .map_err(|err| format!("invalid sender secret key: {}", err))?; + // Build ScriptUnlocker + let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![sender_key]); + let sighash_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>); + let sighash_script_id = ScriptId::new_type(SIGHASH_TYPE_HASH.clone()); + let mut unlockers = HashMap::default(); + unlockers.insert( + sighash_script_id, + Box::new(sighash_unlocker) as Box, + ); + + // Build the transaction + // let output = CellOutput::new_builder() + // .lock(Script::from(&args.receiver)) + // .capacity(args.capacity.0.pack()) + // .build(); + // let builder = CapacityTransferBuilder::new(vec![(output, Bytes::default())]); + // let (tx, still_locked_groups) = builder.build_unlocked( + // &mut cell_collector, + // &cell_dep_resolver, + // &header_dep_resolver, + // &tx_dep_provider, + // &balancer, + // &unlockers, + // )?; + + let tx_dep_provider = DefaultTransactionDependencyProvider::new(args.ckb_rpc.as_str(), 10); + let (new_tx, new_still_locked_groups) = unlock_tx(tx, &tx_dep_provider, &unlockers)?; + Ok((new_tx, new_still_locked_groups)) +} diff --git a/src/test-data/omni_lock b/src/test-data/omni_lock index 96c90c33..9689bbaf 100755 Binary files a/src/test-data/omni_lock and b/src/test-data/omni_lock differ diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 73a46a43..bce3ff3e 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1029,11 +1029,7 @@ fn test_udt_transfer() { ctx.add_live_cell(receiver_input, receiver_output.clone(), receiver_data, None); let udt_receiver = UdtTargetReceiver::new(TransferAction::Update, receiver_acp_lock, 300); - let builder = UdtTransferBuilder { - type_script, - sender: sender.clone(), - receivers: vec![udt_receiver], - }; + let builder = UdtTransferBuilder::new(type_script, sender.clone(), vec![udt_receiver]); let placeholder_witness = WitnessArgs::new_builder() .lock(Some(Bytes::from(vec![0u8; 65])).pack()) .build(); @@ -1087,3 +1083,4 @@ fn test_udt_transfer() { pub mod omni_lock; pub mod omni_lock_util; +pub mod opentx; diff --git a/src/tests/omni_lock.rs b/src/tests/omni_lock.rs index cdcd3c73..1fce0292 100644 --- a/src/tests/omni_lock.rs +++ b/src/tests/omni_lock.rs @@ -40,9 +40,9 @@ use rand::Rng; use crate::tx_builder::{unlock_tx, CapacityBalancer, TxBuilder}; -const OMNILOCK_BIN: &[u8] = include_bytes!("../test-data/omni_lock"); +pub const OMNILOCK_BIN: &[u8] = include_bytes!("../test-data/omni_lock"); -fn build_omnilock_script(cfg: &OmniLockConfig) -> Script { +pub fn build_omnilock_script(cfg: &OmniLockConfig) -> Script { let omnilock_data_hash = H256::from(blake2b_256(OMNILOCK_BIN)); Script::new_builder() .code_hash(omnilock_data_hash.pack()) @@ -51,7 +51,7 @@ fn build_omnilock_script(cfg: &OmniLockConfig) -> Script { .build() } -fn build_omnilock_unlockers( +pub fn build_omnilock_unlockers( key: secp256k1::SecretKey, config: OmniLockConfig, unlock_mode: OmniUnlockMode, @@ -117,13 +117,12 @@ fn test_omnilock_simple_hash(cfg: OmniLockConfig) { CapacityBalancer::new_simple(sender.clone(), placeholder_witness.clone(), FEE_RATE); let mut cell_collector = ctx.to_live_cells_context(); - let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); - let unlockers = build_omnilock_unlockers(account2_key, cfg.clone(), unlock_mode); + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(account0_key, cfg, unlock_mode); let mut tx = builder .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) .unwrap(); - let unlockers = build_omnilock_unlockers(account2_key, cfg, unlock_mode); let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); assert!(new_locked_groups.is_empty()); tx = new_tx; @@ -1172,11 +1171,7 @@ fn test_omnilock_udt_transfer() { ctx.add_live_cell(receiver_input, receiver_output.clone(), receiver_data, None); let udt_receiver = UdtTargetReceiver::new(TransferAction::Update, receiver_acp_lock, 300); - let builder = UdtTransferBuilder { - type_script, - sender: sender.clone(), - receivers: vec![udt_receiver], - }; + let builder = UdtTransferBuilder::new(type_script, sender.clone(), vec![udt_receiver]); let placeholder_witness = WitnessArgs::new_builder() .lock(Some(Bytes::from(vec![0u8; 65])).pack()) .build(); diff --git a/src/tests/opentx.rs b/src/tests/opentx.rs new file mode 100644 index 00000000..bdbdd646 --- /dev/null +++ b/src/tests/opentx.rs @@ -0,0 +1,751 @@ +use ckb_hash::blake2b_256; +use ckb_jsonrpc_types as json_types; +use std::collections::HashMap; + +use crate::{ + constants::{ONE_CKB, SIGHASH_TYPE_HASH}, + test_util::random_out_point, + tests::{ + build_sighash_script, init_context, + omni_lock::{build_omnilock_script, build_omnilock_unlockers, OMNILOCK_BIN}, + ACCOUNT0_ARG, ACCOUNT0_KEY, ACCOUNT1_ARG, ACCOUNT1_KEY, ACCOUNT2_ARG, ACCOUNT2_KEY, + ACCOUNT3_ARG, ACCOUNT3_KEY, FEE_RATE, SUDT_BIN, + }, + traits::{CellCollector, CellQueryOptions, SecpCkbRawKeySigner}, + tx_builder::{ + omni_lock::OmniLockTransferBuilder, transfer::CapacityTransferBuilder, + udt::UdtTransferBuilder, + }, + unlock::{ + opentx::OpentxWitness, MultisigConfig, OmniLockConfig, OmniUnlockMode, ScriptUnlocker, + SecpSighashUnlocker, + }, + util::{blake160, keccak160}, + ScriptId, +}; + +use ckb_crypto::secp::{Pubkey, SECP256K1}; +use ckb_types::{ + bytes::Bytes, + core::{Capacity, ScriptHashType}, + packed::{CellInput, CellOutput, Script, WitnessArgs}, + prelude::*, + H160, H256, +}; +use rand::Rng; + +use crate::tx_builder::{unlock_tx, CapacityBalancer, TxBuilder}; +const ZERO_FEE_RATE: u64 = 0; + +fn build_simple_config(key: H256) -> OmniLockConfig { + let priv_key = secp256k1::SecretKey::from_slice(key.as_bytes()) + .map_err(|err| format!("invalid sender secret key: {}", err)) + .unwrap(); + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &priv_key); + OmniLockConfig::new_pubkey_hash(blake160(&pubkey.serialize())) +} +#[test] +fn test_opentx_pay_from_sighash() { + let cfg = build_simple_config(ACCOUNT0_KEY); + test_opentx_pay_simple_hash(cfg); +} + +#[test] +fn test_opentx_pay_from_ethereum() { + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &account0_key); + let cfg = OmniLockConfig::new_ethereum(keccak160(Pubkey::from(pubkey).as_ref())); + test_opentx_pay_simple_hash(cfg); +} + +/// account0(200) => account0(exchange 199) + open pay 1, +/// account2(100) => account2(101 - transaction fee) +fn test_opentx_pay_simple_hash(mut cfg: OmniLockConfig) { + cfg.set_opentx_mode(); + let unlock_mode = OmniUnlockMode::Normal; + let sender = build_omnilock_script(&cfg); + let receiver = build_sighash_script(ACCOUNT2_ARG); + + let ctx = init_context( + vec![(OMNILOCK_BIN, true)], + vec![ + (sender.clone(), Some(200 * ONE_CKB)), + (receiver.clone(), Some(100 * ONE_CKB)), + (receiver.clone(), Some(200 * ONE_CKB)), + ], + ); + + let output = CellOutput::new_builder() + .capacity((199 * ONE_CKB).pack()) + .lock(sender.clone()) + .build(); + let builder = OmniLockTransferBuilder::new_open( + ONE_CKB.into(), + vec![(output.clone(), Bytes::default())], + cfg.clone(), + None, + ); + let placeholder_witness = cfg.placeholder_witness(unlock_mode).unwrap(); + let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE); + + let mut cell_collector = ctx.to_live_cells_context(); + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(account0_key, cfg.clone(), unlock_mode); + let mut tx = builder + .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + let mut rng = rand::thread_rng(); + let salt: u32 = rng.gen(); + let wit = OpentxWitness::new_sig_all_relative(&tx, Some(salt)).unwrap(); + cfg.set_opentx_input(wit); + tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &cfg, + OmniUnlockMode::Normal, + &ctx, + &sender, + ) + .unwrap(); + // config updated, so unlockers must rebuilt. + let unlockers = build_omnilock_unlockers(account0_key, cfg.clone(), unlock_mode); + let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); + assert!(new_locked_groups.is_empty()); + tx = new_tx; + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + // use the opentx + + // Build ScriptUnlocker + let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap(); + let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![account2_key]); + let sighash_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>); + let sighash_script_id = ScriptId::new_type(SIGHASH_TYPE_HASH.clone()); + let mut unlockers = HashMap::default(); + unlockers.insert( + sighash_script_id, + Box::new(sighash_unlocker) as Box, + ); + + // Build CapacityBalancer + let placeholder_witness = WitnessArgs::new_builder() + .lock(Some(Bytes::from(vec![0u8; 65])).pack()) + .build(); + let balancer = CapacityBalancer::new_simple(receiver.clone(), placeholder_witness, 1000); + // // Build the transaction + let query = CellQueryOptions::new_lock(receiver.clone()); + let (inputs, total_capacity) = cell_collector.collect_live_cells(&query, false).unwrap(); + let input = &inputs[0]; + let input_output = &input.out_point; + println!("{:#x} total_capacity: {}", input_output, total_capacity); + // let output = CellOutput::new_builder() + // .lock(receiver.clone()) + // .capacity((100 * ONE_CKB).pack()) + // .build(); + let builder = CapacityTransferBuilder::new_with_transaction( + vec![/*(output.clone(), Bytes::default())*/], + tx, + ); + let (tx, still_locked_groups) = builder + .build_unlocked(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + assert_eq!(1, still_locked_groups.len()); + + assert_eq!(tx.header_deps().len(), 0); + assert_eq!(tx.cell_deps().len(), 2); + assert_eq!(tx.inputs().len(), 2); + assert_eq!(tx.outputs().len(), 2); + assert_eq!(tx.output(0).unwrap(), output); + let output1 = tx.output(1).unwrap(); + assert_eq!(output1.lock(), receiver); + let receiver_capacity: u64 = output1.capacity().unpack(); + assert!(receiver_capacity - 100 * ONE_CKB < ONE_CKB); + assert_eq!(tx.witnesses().len(), 2); + ctx.verify(tx, FEE_RATE).unwrap(); +} + +/// multisig(200) => multisig(exchange 199) + open pay 1, locked by account0, account1, account2 +/// account3(400) => account2(401 - transaction fee) +#[test] +fn test_opentx_pay_from_multisig() { + let unlock_mode = OmniUnlockMode::Normal; + let lock_args = vec![ + ACCOUNT0_ARG.clone(), + ACCOUNT1_ARG.clone(), + ACCOUNT2_ARG.clone(), + ]; + let multi_cfg = MultisigConfig::new_with(lock_args, 0, 2).unwrap(); + let mut cfg = OmniLockConfig::new_multisig(multi_cfg); + cfg.set_opentx_mode(); + + let sender = build_omnilock_script(&cfg); + let receiver = build_sighash_script(ACCOUNT3_ARG); + + let ctx = init_context( + vec![(OMNILOCK_BIN, true)], + vec![ + (sender.clone(), Some(200 * ONE_CKB)), + (sender.clone(), Some(300 * ONE_CKB)), + (receiver.clone(), Some(400 * ONE_CKB)), + (receiver.clone(), Some(500 * ONE_CKB)), + (receiver.clone(), Some(600 * ONE_CKB)), + ], + ); + + let output = CellOutput::new_builder() + .capacity((199 * ONE_CKB).pack()) + .lock(sender.clone()) + .build(); + let builder = OmniLockTransferBuilder::new_open( + ONE_CKB.into(), + vec![(output.clone(), Bytes::default())], + cfg.clone(), + None, + ); + let placeholder_witness = cfg.placeholder_witness(unlock_mode).unwrap(); + let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE); + + let mut cell_collector = ctx.to_live_cells_context(); + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(account0_key, cfg.clone(), unlock_mode); + let mut tx = builder + .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + // add opentx hash data + let mut rng = rand::thread_rng(); + let salt: u32 = rng.gen(); + let wit = OpentxWitness::new_sig_all_relative(&tx, Some(salt)).unwrap(); + cfg.set_opentx_input(wit); + tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &cfg, + OmniUnlockMode::Normal, + &ctx, + &sender, + ) + .unwrap(); + for key in [account0_key, account2_key] { + let unlockers = build_omnilock_unlockers(key, cfg.clone(), unlock_mode); + let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); + assert!(new_locked_groups.is_empty()); + tx = new_tx; + } + + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + // use the opentx + + // Build ScriptUnlocker + let account3_key = secp256k1::SecretKey::from_slice(ACCOUNT3_KEY.as_bytes()).unwrap(); + let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![account3_key]); + let sighash_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>); + let sighash_script_id = ScriptId::new_type(SIGHASH_TYPE_HASH.clone()); + let mut unlockers = HashMap::default(); + unlockers.insert( + sighash_script_id, + Box::new(sighash_unlocker) as Box, + ); + + // Build CapacityBalancer + let placeholder_witness = WitnessArgs::new_builder() + .lock(Some(Bytes::from(vec![0u8; 65])).pack()) + .build(); + let balancer = + CapacityBalancer::new_simple(receiver.clone(), placeholder_witness.clone(), 1000); + // // Build the transaction + let query = CellQueryOptions::new_lock(receiver.clone()); + let (inputs, total_capacity) = cell_collector.collect_live_cells(&query, false).unwrap(); + let input = &inputs[0]; + let input_output = &input.out_point; + println!("{:#x} total_capacity: {}", input_output, total_capacity); + // let output = CellOutput::new_builder() + // .lock(receiver.clone()) + // .capacity((100 * ONE_CKB).pack()) + // .build(); + let builder = CapacityTransferBuilder::new_with_transaction( + vec![/*(output.clone(), Bytes::default())*/], + tx, + ); + let (tx, still_locked_groups) = builder + .build_unlocked(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + assert_eq!(1, still_locked_groups.len()); + assert_eq!(tx.header_deps().len(), 0); + assert_eq!(tx.cell_deps().len(), 2); + assert_eq!(tx.inputs().len(), 2); + assert_eq!(tx.outputs().len(), 2); + assert_eq!(tx.output(0).unwrap(), output); + let output1 = tx.output(1).unwrap(); + assert_eq!(output1.lock(), receiver); + let receiver_capacity: u64 = output1.capacity().unpack(); + assert!(receiver_capacity - 400 * ONE_CKB < ONE_CKB); + let witnesses = tx + .witnesses() + .into_iter() + .map(|w| w.raw_data()) + .collect::>(); + assert_eq!(witnesses.len(), 2); + assert_eq!(witnesses[1].len(), placeholder_witness.as_slice().len()); + ctx.verify(tx, FEE_RATE).unwrap(); +} + +#[test] +fn test_opentx_pay_receive_sighash_absolute_from_start() { + test_opentx_pay_receive_sighash_absolute(true); +} +#[test] +fn test_opentx_pay_receive_sighash_absolute_self() { + test_opentx_pay_receive_sighash_absolute(false); +} +fn test_opentx_pay_receive_sighash_absolute(from_start: bool) { + let sender_cfg = build_simple_config(ACCOUNT0_KEY); + let receiver_cfg = build_simple_config(ACCOUNT2_KEY); + test_opentx_pay_receive_simple_hash_absolute(sender_cfg, receiver_cfg, from_start); +} + +#[test] +fn test_opentx_pay_receive_ethereum_absolute_from_start() { + test_opentx_pay_receive_ethereum_absolute(true); +} +#[test] +fn test_opentx_pay_receive_ethereum_absolute_from_self() { + test_opentx_pay_receive_ethereum_absolute(false); +} +fn test_opentx_pay_receive_ethereum_absolute(from_start: bool) { + let cfgs: Vec = [ACCOUNT0_KEY, ACCOUNT2_KEY] + .iter() + .map(|key| { + let priv_key = secp256k1::SecretKey::from_slice(key.as_bytes()).unwrap(); + let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &priv_key); + OmniLockConfig::new_ethereum(keccak160(Pubkey::from(pubkey).as_ref())) + }) + .collect(); + test_opentx_pay_receive_simple_hash_absolute(cfgs[0].clone(), cfgs[1].clone(), from_start); +} + +/// account0(200) => account0(exchange 199) + open pay 1, +/// account2(100) => account2(101 - transaction fee) +fn test_opentx_pay_receive_simple_hash_absolute( + mut sender_cfg: OmniLockConfig, + mut receiver_cfg: OmniLockConfig, + from_start: bool, +) { + sender_cfg.set_opentx_mode(); + receiver_cfg.set_opentx_mode(); + let unlock_mode = OmniUnlockMode::Normal; + let sender = build_omnilock_script(&sender_cfg); + let receiver = build_omnilock_script(&receiver_cfg); + + let ctx = init_context( + vec![(OMNILOCK_BIN, true)], + vec![ + (sender.clone(), Some(200 * ONE_CKB)), + (receiver.clone(), Some(100 * ONE_CKB)), + (receiver.clone(), Some(200 * ONE_CKB)), + ], + ); + + let output = CellOutput::new_builder() + .capacity((199 * ONE_CKB).pack()) + .lock(sender.clone()) + .build(); + let builder = OmniLockTransferBuilder::new_open( + (ONE_CKB).into(), + vec![(output.clone(), Bytes::default())], + sender_cfg.clone(), + None, + ); + let placeholder_witness = sender_cfg.placeholder_witness(unlock_mode).unwrap(); + let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE); + + let mut cell_collector = ctx.to_live_cells_context(); + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(account0_key, sender_cfg.clone(), unlock_mode); + let mut tx = builder + .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + let mut rng = rand::thread_rng(); + let salt: u32 = rng.gen(); + let wit = OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap(); + sender_cfg.set_opentx_input(wit); + tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &sender_cfg, + OmniUnlockMode::Normal, + &ctx, + &sender, + ) + .unwrap(); + // config updated, so unlockers must rebuilt. + let unlockers = build_omnilock_unlockers(account0_key, sender_cfg.clone(), unlock_mode); + let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); + assert!(new_locked_groups.is_empty()); + tx = new_tx; + + // use the opentx + let opentx_input_len = tx.inputs().len(); + let opentx_output_len = tx.outputs().len(); + receiver_cfg.set_opentx_reserve_bytes_by_commands(20); + // Build ScriptUnlocker + let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(account2_key, receiver_cfg.clone(), unlock_mode); + + // Build CapacityBalancer + let placeholder_witness = receiver_cfg.placeholder_witness(unlock_mode).unwrap(); + // why + 100? After update openwitness input list, will need tens of bytes more, if not +100, after update, should calculate adjust the fee again. + // If adjust the transaction fee later, the exchange may mot be enough to maintain the minimal capacity. + let balancer = CapacityBalancer::new_simple(receiver.clone(), placeholder_witness, FEE_RATE); + + let builder = CapacityTransferBuilder::new_with_transaction( + vec![/*(output.clone(), Bytes::default())*/], + tx, + ); + let mut tx = builder + .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + assert_eq!(opentx_input_len + 1, tx.inputs().len()); + assert_eq!(opentx_output_len + 1, tx.outputs().len()); + + let salt: u32 = rng.gen(); + let mut wit = if from_start { + OpentxWitness::new_sig_all_absolute(&tx, Some(salt)) + } else { + OpentxWitness::new_sig_to_end_absolute(&tx, Some(salt), opentx_input_len, opentx_output_len) + } + .unwrap(); //OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap(); + wit.add_tx_hash_input(); + receiver_cfg.set_opentx_input(wit); + + tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &receiver_cfg, + OmniUnlockMode::Normal, + &ctx, + &receiver, + ) + .unwrap(); + + // config updated, so unlockers must rebuilt. + let unlockers = build_omnilock_unlockers(account2_key, receiver_cfg.clone(), unlock_mode); + let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); + + assert_eq!(1, new_locked_groups.len()); + tx = new_tx; + + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + + assert_eq!(tx.header_deps().len(), 0); + assert_eq!(tx.cell_deps().len(), 1); + assert_eq!(tx.inputs().len(), 2); + assert_eq!(tx.outputs().len(), 2); + assert_eq!(tx.output(0).unwrap(), output); + let output1 = tx.output(1).unwrap(); + assert_eq!(output1.lock(), receiver); + let receiver_capacity: u64 = output1.capacity().unpack(); + assert!(receiver_capacity - 100 * ONE_CKB < ONE_CKB); + assert_eq!(tx.witnesses().len(), 2); + ctx.verify(tx, FEE_RATE).unwrap(); +} +#[test] +fn test_opentx_pay_receive_multisig_absolute_from_start() { + test_opentx_pay_receive_multisig_absolute(true); +} + +#[test] +fn test_opentx_pay_receive_multisig_absolute_from_self() { + test_opentx_pay_receive_multisig_absolute(false); +} + +/// multisig(200) => multisig(exchange 199) + open pay 1, locked by account0, account1, account2 +/// account3(400) => account2(401 - transaction fee) +fn test_opentx_pay_receive_multisig_absolute(from_start: bool) { + let unlock_mode = OmniUnlockMode::Normal; + let lock_args = vec![ + ACCOUNT0_ARG.clone(), + ACCOUNT1_ARG.clone(), + ACCOUNT2_ARG.clone(), + ]; + let multi_cfg = MultisigConfig::new_with(lock_args, 0, 2).unwrap(); + let mut sender_cfg = OmniLockConfig::new_multisig(multi_cfg); + sender_cfg.set_opentx_mode(); + + let sender = build_omnilock_script(&sender_cfg); + let lock_args = vec![ + ACCOUNT1_ARG.clone(), + ACCOUNT2_ARG.clone(), + ACCOUNT3_ARG.clone(), + ]; + let multi_cfg = MultisigConfig::new_with(lock_args, 0, 2).unwrap(); + let mut receiver_cfg = OmniLockConfig::new_multisig(multi_cfg); + receiver_cfg.set_opentx_mode(); + let receiver = build_omnilock_script(&receiver_cfg); + + let ctx = init_context( + vec![(OMNILOCK_BIN, true)], + vec![ + (sender.clone(), Some(200 * ONE_CKB)), + (sender.clone(), Some(300 * ONE_CKB)), + (receiver.clone(), Some(400 * ONE_CKB)), + (receiver.clone(), Some(500 * ONE_CKB)), + (receiver.clone(), Some(600 * ONE_CKB)), + ], + ); + + let output = CellOutput::new_builder() + .capacity((199 * ONE_CKB).pack()) + .lock(sender.clone()) + .build(); + let builder = OmniLockTransferBuilder::new_open( + ONE_CKB.into(), + vec![(output.clone(), Bytes::default())], + sender_cfg.clone(), + None, + ); + let placeholder_witness = sender_cfg.placeholder_witness(unlock_mode).unwrap(); + let balancer = CapacityBalancer::new_simple(sender.clone(), placeholder_witness, ZERO_FEE_RATE); + + let mut cell_collector = ctx.to_live_cells_context(); + let account0_key = secp256k1::SecretKey::from_slice(ACCOUNT0_KEY.as_bytes()).unwrap(); + let account2_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(account0_key, sender_cfg.clone(), unlock_mode); + let mut tx = builder + .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + // add opentx hash data + let mut rng = rand::thread_rng(); + let salt: u32 = rng.gen(); + let wit = OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap(); + sender_cfg.set_opentx_input(wit); + tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &sender_cfg, + OmniUnlockMode::Normal, + &ctx, + &sender, + ) + .unwrap(); + for key in [account0_key, account2_key] { + let unlockers = build_omnilock_unlockers(key, sender_cfg.clone(), unlock_mode); + let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); + assert!(new_locked_groups.is_empty()); + tx = new_tx; + } + + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + // use the opentx + let opentx_input_len = tx.inputs().len(); + let opentx_output_len = tx.outputs().len(); + receiver_cfg.set_opentx_reserve_bytes_by_commands(20); + // Build ScriptUnlocker + let account1_key = secp256k1::SecretKey::from_slice(ACCOUNT1_KEY.as_bytes()).unwrap(); + let account3_key = secp256k1::SecretKey::from_slice(ACCOUNT3_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(account1_key, receiver_cfg.clone(), unlock_mode); + // Build CapacityBalancer + let placeholder_witness = receiver_cfg.placeholder_witness(unlock_mode).unwrap(); + let balancer = CapacityBalancer::new_simple(receiver.clone(), placeholder_witness, FEE_RATE); + + let builder = CapacityTransferBuilder::new_with_transaction(vec![], tx); + let mut tx = builder + .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + assert_eq!(opentx_input_len + 1, tx.inputs().len()); + assert_eq!(opentx_output_len + 1, tx.outputs().len()); + + let salt: u32 = rng.gen(); + let mut wit = if from_start { + OpentxWitness::new_sig_all_absolute(&tx, Some(salt)) + } else { + OpentxWitness::new_sig_to_end_absolute(&tx, Some(salt), opentx_input_len, opentx_output_len) + } + .unwrap(); //OpentxWitness::new_sig_all_absolute(&tx, Some(salt)).unwrap(); + wit.add_tx_hash_input(); + receiver_cfg.set_opentx_input(wit); + + tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &receiver_cfg, + OmniUnlockMode::Normal, + &ctx, + &receiver, + ) + .unwrap(); + + for key in [account1_key, account3_key] { + let unlockers = build_omnilock_unlockers(key, receiver_cfg.clone(), unlock_mode); + let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); + assert_eq!(1, new_locked_groups.len()); + tx = new_tx; + } + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + assert_eq!(tx.header_deps().len(), 0); + assert_eq!(tx.cell_deps().len(), 1); + assert_eq!(tx.inputs().len(), 2); + assert_eq!(tx.outputs().len(), 2); + assert_eq!(tx.output(0).unwrap(), output); + let output1 = tx.output(1).unwrap(); + assert_eq!(output1.lock(), receiver); + let receiver_capacity: u64 = output1.capacity().unpack(); + assert!(receiver_capacity - 400 * ONE_CKB < ONE_CKB); + + assert_eq!(tx.witnesses().len(), 2); + ctx.verify(tx, FEE_RATE).unwrap(); +} + +#[test] +fn test_opentx_udt_open_buy() { + // ACCOUNT1(alice) will spend 50.01 CKB with fee to buy 1,000,000 SUDT + // ACCOUNT2(bob) collect the 50 CKB with the transfer 1,000,000 SUDT + let unlock_mode = OmniUnlockMode::Normal; + let mut alice_cfg = build_simple_config(ACCOUNT1_KEY); + alice_cfg.set_opentx_mode(); + let alice = build_omnilock_script(&alice_cfg); + let bob = build_sighash_script(ACCOUNT2_ARG); + + let mut ctx = init_context( + vec![(OMNILOCK_BIN, true), (SUDT_BIN, false)], + vec![ + (alice.clone(), Some(300 * ONE_CKB)), + (bob.clone(), Some(400 * ONE_CKB)), + ], + ); + let sudt_data_hash = H256::from(blake2b_256(SUDT_BIN)); + let owner = build_sighash_script(H160::default()); + let type_script = Script::new_builder() + .code_hash(sudt_data_hash.pack()) + .hash_type(ScriptHashType::Data1.into()) + .args(owner.calc_script_hash().as_bytes().pack()) + .build(); + let sudt_input = CellInput::new(random_out_point(), 0); + let sudt_output = CellOutput::new_builder() + .capacity(ONE_CKB.pack()) + .lock(bob.clone()) + .type_(Some(type_script.clone()).pack()) + .build(); + let sudt_capacity = sudt_output + .occupied_capacity(Capacity::bytes(16).unwrap()) + .unwrap() + .as_u64(); + println!("sudt_capacity: {}", sudt_capacity); + let sudt_output = sudt_output + .as_builder() + .capacity(sudt_capacity.pack()) + .build(); + let sudt_data = Bytes::from(1_000_000u128.to_le_bytes().to_vec()); + ctx.add_live_cell(sudt_input, sudt_output, sudt_data.clone(), None); + + let fee = 100_0000u64; + // build opentx alice's input + let builder = OmniLockTransferBuilder::new_open( + (50 * ONE_CKB + sudt_capacity + fee).into(), + vec![], + alice_cfg.clone(), + None, + ); + let placeholder_witness = alice_cfg.placeholder_witness(unlock_mode).unwrap(); + let balancer = CapacityBalancer::new_simple(alice.clone(), placeholder_witness, ZERO_FEE_RATE); + + let mut cell_collector = ctx.to_live_cells_context(); + let alice_key = secp256k1::SecretKey::from_slice(ACCOUNT1_KEY.as_bytes()).unwrap(); + let unlockers = build_omnilock_unlockers(alice_key, alice_cfg.clone(), unlock_mode); + let mut tx = builder + .build_balanced(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + // add sudt output + let sudt_output = CellOutput::new_builder() + .capacity((sudt_capacity).pack()) + .lock(alice.clone()) + .type_(Some(type_script.clone()).pack()) + .build(); + tx = tx + .as_advanced_builder() + .output(sudt_output.clone()) + .output_data(sudt_data.pack()) + .build(); + // update opentx input list + let mut rng = rand::thread_rng(); + let salt: u32 = rng.gen(); + let wit = OpentxWitness::new_sig_all_relative(&tx, Some(salt)).unwrap(); + alice_cfg.set_opentx_input(wit); + tx = OmniLockTransferBuilder::update_opentx_witness( + tx, + &alice_cfg, + OmniUnlockMode::Normal, + &ctx, + &alice, + ) + .unwrap(); + // config updated, so unlockers must rebuilt. + let unlockers = build_omnilock_unlockers(alice_key, alice_cfg.clone(), unlock_mode); + let (new_tx, new_locked_groups) = unlock_tx(tx.clone(), &ctx, &unlockers).unwrap(); + assert!(new_locked_groups.is_empty()); + tx = new_tx; + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + // use opentx + let builder = UdtTransferBuilder::new_with_transaction(type_script, bob.clone(), vec![], tx); + let placeholder_witness = WitnessArgs::new_builder() + .lock(Some(Bytes::from(vec![0u8; 65])).pack()) + .build(); + let balancer = CapacityBalancer::new_simple(bob, placeholder_witness, FEE_RATE); + + let bob_key = secp256k1::SecretKey::from_slice(ACCOUNT2_KEY.as_bytes()).unwrap(); + let signer = SecpCkbRawKeySigner::new_with_secret_keys(vec![bob_key]); + let script_unlocker = SecpSighashUnlocker::from(Box::new(signer) as Box<_>); + let mut unlockers: HashMap> = HashMap::default(); + unlockers.insert( + ScriptId::new_type(SIGHASH_TYPE_HASH.clone()), + Box::new(script_unlocker), + ); + + let mut cell_collector = ctx.to_live_cells_context(); + let (tx, locked_groups) = builder + .build_unlocked(&mut cell_collector, &ctx, &ctx, &ctx, &balancer, &unlockers) + .unwrap(); + println!( + "> tx: {}", + serde_json::to_string_pretty(&json_types::TransactionView::from(tx.clone())).unwrap() + ); + assert_eq!(locked_groups.len(), 1); + assert_eq!(tx.header_deps().len(), 0); + assert_eq!(tx.cell_deps().len(), 3); + assert_eq!(tx.inputs().len(), 3); + let outputs = tx.outputs().into_iter().collect::>(); + assert_eq!(outputs.len(), 4); + assert_eq!(outputs[1], sudt_output); + let expected_outputs_data = vec![ + Bytes::from(1_000_000u128.to_le_bytes().to_vec()), + Bytes::from(0u128.to_le_bytes().to_vec()), + ]; + let outputs_data = tx + .outputs_data() + .into_iter() + .map(|d| d.raw_data()) + .collect::>(); + assert_eq!(outputs_data[1..3], expected_outputs_data); + ctx.verify(tx, FEE_RATE).unwrap(); +} diff --git a/src/tx_builder/mod.rs b/src/tx_builder/mod.rs index ae26f0f8..4981f52e 100644 --- a/src/tx_builder/mod.rs +++ b/src/tx_builder/mod.rs @@ -164,6 +164,14 @@ pub fn tx_fee( tx: TransactionView, tx_dep_provider: &dyn TransactionDependencyProvider, header_dep_resolver: &dyn HeaderDepResolver, +) -> Result { + tx_fee_with_open(tx, tx_dep_provider, header_dep_resolver, 0) +} +pub fn tx_fee_with_open( + tx: TransactionView, + tx_dep_provider: &dyn TransactionDependencyProvider, + header_dep_resolver: &dyn HeaderDepResolver, + open_capacity: u64, ) -> Result { let mut input_total: u64 = 0; for input in tx.inputs() { @@ -218,7 +226,7 @@ pub fn tx_fee( }; input_total += capacity; } - let output_total = tx.outputs_capacity()?.as_u64(); + let output_total = tx.outputs_capacity()?.as_u64() + open_capacity; #[allow(clippy::unnecessary_lazy_evaluations)] input_total .checked_sub(output_total) @@ -359,6 +367,26 @@ pub fn balance_tx_capacity( tx_dep_provider: &dyn TransactionDependencyProvider, cell_dep_resolver: &dyn CellDepResolver, header_dep_resolver: &dyn HeaderDepResolver, +) -> Result { + balance_tx_capacity_with_open( + tx, + balancer, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + 0, + ) +} +/// Fill more inputs to balance the transaction capacity +pub fn balance_tx_capacity_with_open( + tx: &TransactionView, + balancer: &CapacityBalancer, + cell_collector: &mut dyn CellCollector, + tx_dep_provider: &dyn TransactionDependencyProvider, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + open_capacity: u64, ) -> Result { let capacity_provider = &balancer.capacity_provider; if capacity_provider.lock_scripts.is_empty() { @@ -403,6 +431,7 @@ pub fn balance_tx_capacity( let cell = tx_dep_provider.get_cell(&input.previous_output())?; if cell.lock() == *lock_script { has_provider = true; + break; } } while tx.witnesses().item_count() + witnesses.len() @@ -430,8 +459,12 @@ pub fn balance_tx_capacity( let tx_size = new_tx.data().as_reader().serialized_size_in_block(); let min_fee = balancer.fee_rate.fee(tx_size).as_u64(); let mut need_more_capacity = 1; - let fee_result: Result = - tx_fee(new_tx.clone(), tx_dep_provider, header_dep_resolver); + let fee_result: Result = tx_fee_with_open( + new_tx.clone(), + tx_dep_provider, + header_dep_resolver, + open_capacity, + ); match fee_result { Ok(fee) if fee == min_fee => { return Ok(new_tx); diff --git a/src/tx_builder/omni_lock.rs b/src/tx_builder/omni_lock.rs index 2662f030..ac5d0607 100644 --- a/src/tx_builder/omni_lock.rs +++ b/src/tx_builder/omni_lock.rs @@ -1,24 +1,28 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use ckb_types::{ bytes::Bytes, core::{DepType, TransactionBuilder, TransactionView}, - packed::{CellDep, CellInput, CellOutput, OutPoint}, + packed::{CellDep, CellInput, CellOutput, OutPoint, Script}, prelude::*, }; -use super::{TxBuilder, TxBuilderError}; -use crate::types::ScriptId; +use super::{ + balance_tx_capacity_with_open, fill_placeholder_witnesses, CapacityBalancer, TxBuilder, + TxBuilderError, +}; use crate::{ traits::{CellCollector, CellDepResolver, HeaderDepResolver, TransactionDependencyProvider}, - unlock::OmniLockConfig, + unlock::{omni_lock::ConfigError, OmniLockConfig, OmniUnlockMode, ScriptUnlocker}, }; +use crate::{types::ScriptId, HumanCapacity}; /// A builder to build an omnilock transfer transaction. pub struct OmniLockTransferBuilder { pub outputs: Vec<(CellOutput, Bytes)>, pub cfg: OmniLockConfig, pub rce_cells: Option>, + pub open_out_capacity: HumanCapacity, } impl OmniLockTransferBuilder { @@ -31,8 +35,57 @@ impl OmniLockTransferBuilder { outputs, cfg, rce_cells, + open_out_capacity: HumanCapacity(0), + } + } + + /// Create an OmniLockTransferBuilder with open out in the output list. + /// After the transaction built, the open out should be removed. + pub fn new_open( + open_out_capacity: HumanCapacity, + outputs: Vec<(CellOutput, Bytes)>, + cfg: OmniLockConfig, + rce_cells: Option>, + ) -> OmniLockTransferBuilder { + OmniLockTransferBuilder { + outputs, + cfg, + rce_cells, + open_out_capacity, } } + + /// after the open transaction input list updated(exclude base input/output), the witness should be updated + pub fn update_opentx_witness( + tx: TransactionView, + omnilock_config: &OmniLockConfig, + unlock_mode: OmniUnlockMode, + tx_dep_provider: &dyn TransactionDependencyProvider, + sender: &Script, + ) -> Result { + // after set opentx config, need to update the witness field + let placeholder_witness = omnilock_config.placeholder_witness(unlock_mode)?; + let tmp_idxes: Vec<_> = tx + .input_pts_iter() + .enumerate() + .filter(|(_, output)| tx_dep_provider.get_cell(output).unwrap().lock() == *sender) + .map(|(idx, _)| idx) + .collect(); + let witnesses: Vec<_> = tx + .witnesses() + .into_iter() + .enumerate() + .map(|(i, w)| { + if tmp_idxes.contains(&i) { + placeholder_witness.as_bytes().pack() + } else { + w + } + }) + .collect(); + let tx = tx.as_advanced_builder().set_witnesses(witnesses).build(); + Ok(tx) + } } impl TxBuilder for OmniLockTransferBuilder { @@ -101,4 +154,37 @@ impl TxBuilder for OmniLockTransferBuilder { .set_outputs_data(outputs_data) .build()) } + + /// Build balanced transaction that ready to sign: + /// * Build base transaction + /// * Fill placeholder witness for lock script + /// * balance the capacity + fn build_balanced( + &self, + cell_collector: &mut dyn CellCollector, + cell_dep_resolver: &dyn CellDepResolver, + header_dep_resolver: &dyn HeaderDepResolver, + tx_dep_provider: &dyn TransactionDependencyProvider, + balancer: &CapacityBalancer, + unlockers: &HashMap>, + ) -> Result { + let base_tx = self.build_base( + cell_collector, + cell_dep_resolver, + header_dep_resolver, + tx_dep_provider, + )?; + let (tx_filled_witnesses, _) = + fill_placeholder_witnesses(base_tx, tx_dep_provider, unlockers)?; + let tx = balance_tx_capacity_with_open( + &tx_filled_witnesses, + balancer, + cell_collector, + tx_dep_provider, + cell_dep_resolver, + header_dep_resolver, + self.open_out_capacity.into(), + )?; + Ok(tx) + } } diff --git a/src/tx_builder/transfer.rs b/src/tx_builder/transfer.rs index 7f960e6b..f1053ca0 100644 --- a/src/tx_builder/transfer.rs +++ b/src/tx_builder/transfer.rs @@ -17,11 +17,25 @@ use crate::types::ScriptId; /// will resolve the type script's cell_dep if given. pub struct CapacityTransferBuilder { pub outputs: Vec<(CellOutput, Bytes)>, + pub transaction: Option, } impl CapacityTransferBuilder { pub fn new(outputs: Vec<(CellOutput, Bytes)>) -> CapacityTransferBuilder { - CapacityTransferBuilder { outputs } + CapacityTransferBuilder { + outputs, + transaction: None, + } + } + + pub fn new_with_transaction( + outputs: Vec<(CellOutput, Bytes)>, + transaction: TransactionView, + ) -> CapacityTransferBuilder { + CapacityTransferBuilder { + outputs, + transaction: Some(transaction), + } } } @@ -50,10 +64,15 @@ impl TxBuilder for CapacityTransferBuilder { } } } - Ok(TransactionBuilder::default() - .set_cell_deps(cell_deps.into_iter().collect()) - .set_outputs(outputs) - .set_outputs_data(outputs_data) + let builder = if let Some(tx) = self.transaction.as_ref() { + tx.as_advanced_builder() + } else { + TransactionBuilder::default() + }; + Ok(builder + .cell_deps(cell_deps) + .outputs(outputs) + .outputs_data(outputs_data) .build()) } } diff --git a/src/tx_builder/udt/mod.rs b/src/tx_builder/udt/mod.rs index 4c600f1e..93af71d2 100644 --- a/src/tx_builder/udt/mod.rs +++ b/src/tx_builder/udt/mod.rs @@ -265,6 +265,57 @@ pub struct UdtTransferBuilder { /// The transfer receivers pub receivers: Vec, + + /// The exist transaction + pub transaction: Option, +} + +impl UdtTransferBuilder { + pub fn new(type_script: Script, sender: Script, receivers: Vec) -> Self { + UdtTransferBuilder { + type_script, + sender, + receivers, + transaction: None, + } + } + + pub fn new_with_transaction( + type_script: Script, + sender: Script, + receivers: Vec, + transaction: TransactionView, + ) -> Self { + UdtTransferBuilder { + type_script, + sender, + receivers, + transaction: Some(transaction), + } + } + + fn tx_output_amount(&self) -> u128 { + if self.transaction.is_none() { + return 0; + } + let tx = self.transaction.as_ref().unwrap(); + tx.outputs_with_data_iter() + .filter_map(|(output, data)| { + if data.len() >= 16 + && output.type_().is_some() + && output.type_().to_opt().unwrap() == self.type_script + // && output.lock() == self.sender + { + let mut amount_bytes = [0u8; 16]; + amount_bytes.copy_from_slice(&data.as_ref()[0..16]); + let amount = u128::from_le_bytes(amount_bytes); + Some(amount) + } else { + None + } + }) + .sum() + } } impl TxBuilder for UdtTransferBuilder { @@ -302,6 +353,7 @@ impl TxBuilder for UdtTransferBuilder { amount_bytes.copy_from_slice(&sender_cell.output_data.as_ref()[0..16]); let input_total = u128::from_le_bytes(amount_bytes); let output_total: u128 = self.receivers.iter().map(|receiver| receiver.amount).sum(); + let output_total = output_total + self.tx_output_amount(); if input_total < output_total { return Err(TxBuilderError::Other(anyhow!( "sender udt amount not enough, expected at least: {}, actual: {}", @@ -334,12 +386,16 @@ impl TxBuilder for UdtTransferBuilder { outputs.push(output); outputs_data.push(output_data.pack()); } - - Ok(TransactionBuilder::default() - .set_cell_deps(cell_deps.into_iter().collect()) - .set_inputs(inputs) - .set_outputs(outputs) - .set_outputs_data(outputs_data) + let builder = if let Some(tx) = self.transaction.as_ref() { + tx.as_advanced_builder() + } else { + TransactionBuilder::default() + }; + Ok(builder + .cell_deps(cell_deps) + .inputs(inputs) + .outputs(outputs) + .outputs_data(outputs_data) .build()) } } diff --git a/src/unlock/mod.rs b/src/unlock/mod.rs index edda5d0d..9cc2cdc1 100644 --- a/src/unlock/mod.rs +++ b/src/unlock/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod omni_lock; +pub mod opentx; pub mod rc_data; mod signer; mod unlocker; diff --git a/src/unlock/omni_lock.rs b/src/unlock/omni_lock.rs index 12a31720..86ba6af3 100644 --- a/src/unlock/omni_lock.rs +++ b/src/unlock/omni_lock.rs @@ -2,6 +2,7 @@ use core::hash; use std::fmt::Display; use crate::{ + constants::SECP_SIGNATURE_SIZE, tx_builder::SinceSource, types::{ omni_lock::{Auth, Identity as IdentityType, IdentityOpt, OmniLockWitnessLock}, @@ -22,7 +23,7 @@ use std::convert::TryFrom; use bitflags::bitflags; -use super::{MultisigConfig, OmniUnlockMode}; +use super::{opentx::OpentxWitness, MultisigConfig, OmniUnlockMode}; use thiserror::Error; #[derive( @@ -196,6 +197,8 @@ bitflags! { const TIME_LOCK = 1<<2; /// supply mode, flag is 1<<3, affected args: type script hash for supply const SUPPLY = 1<<3; + /// open transaction mode. + const OPENTX = 1<<4; } } @@ -419,6 +422,11 @@ pub struct OmniLockConfig { time_lock_config: Option, // 32 bytes type script hash info_cell: Option, + + /// open tx config + opentx_input: Option, + /// When do placeholder_witness_lock, opentx_input will be used if it's not None, or this field is used to reserve more capacity. + opentx_reserve_bytes: u32, } impl OmniLockConfig { @@ -442,6 +450,8 @@ impl OmniLockConfig { acp_config: None, time_lock_config: None, info_cell: None, + opentx_input: None, + opentx_reserve_bytes: 0, } } /// Create an ethereum algorithm omnilock with pubkey @@ -488,6 +498,8 @@ impl OmniLockConfig { acp_config: None, time_lock_config: None, info_cell: None, + opentx_input: None, + opentx_reserve_bytes: 0, } } @@ -539,6 +551,48 @@ impl OmniLockConfig { self.info_cell = None; } + /// Set the open transaction input data, and set the OmniLockFlags::OPENTX to omni_lock_flags. + pub fn set_opentx_input(&mut self, opentx_input: OpentxWitness) { + self.omni_lock_flags.set(OmniLockFlags::OPENTX, true); + self.opentx_input = Some(opentx_input); + } + + /// Set the opentx mode, without knowing the open transaction input data yet, can be used to generate lock script args to search avaiable live cells. + pub fn set_opentx_mode(&mut self) { + self.omni_lock_flags.set(OmniLockFlags::OPENTX, true); + } + + /// Check if it contains open transaction mode + pub fn is_opentx_mode(&self) -> bool { + self.omni_lock_flags.contains(OmniLockFlags::OPENTX) + } + + /// Clear the open transaction input data, and clear OmniLockFlags::OPENTX from omni_lock_flags. + pub fn clear_opentx_input(&mut self) { + self.omni_lock_flags.set(OmniLockFlags::OPENTX, false); + self.opentx_input = None; + } + + pub fn get_opentx_input(&self) -> Option<&OpentxWitness> { + self.opentx_input.as_ref() + } + + /// Set opentx reserve bytes for placeholder witness. + /// # Arguments + /// * `bytes` number of bytes to reserve, the minimual should be 12, and be multiple of 4. + pub fn set_opentx_reserve_bytes(&mut self, bytes: u32) { + self.opentx_reserve_bytes = bytes; + } + + /// Set opentx reserve bytes by possible command numbers. + /// + /// If all data are hashed, the command number verifies depend on input/output numbers, + /// if random salt is used for OpenTxSigInput::new_concat_arg1_arg2, salt bigger than 0xFFFFFF will be 2 commands, + /// salt <= 0xFFFFFF will be one 1 command. + pub fn set_opentx_reserve_bytes_by_commands(&mut self, commands: u32) { + self.set_opentx_reserve_bytes(4 + 4 + 4 * commands); + } + pub fn id(&self) -> &Identity { &self.id } @@ -650,9 +704,18 @@ impl OmniLockConfig { &self, unlock_mode: OmniUnlockMode, ) -> Result { + let mut buf = BytesMut::new(); + if let Some(optx) = self.opentx_input.as_ref() { + buf.extend_from_slice(&optx.to_witness_data()); + } else if self.is_opentx_mode() { + let new_len = buf.len() + self.opentx_reserve_bytes as usize; + buf.resize(new_len, 0u8); + } let mut builder = match self.id.flag { - IdentityFlag::PubkeyHash | IdentityFlag::Ethereum => OmniLockWitnessLock::new_builder() - .signature(Some(Bytes::from(vec![0u8; 65])).pack()), + IdentityFlag::PubkeyHash | IdentityFlag::Ethereum => { + buf.extend_from_slice(&[0u8; SECP_SIGNATURE_SIZE]); + OmniLockWitnessLock::new_builder().signature(Some(buf.freeze()).pack()) + } IdentityFlag::Multisig => { let multisig_config = match unlock_mode { OmniUnlockMode::Admin => self @@ -668,10 +731,15 @@ impl OmniLockConfig { .ok_or(ConfigError::NoMultiSigConfig)?, }; let config_data = multisig_config.to_witness_data(); - let multisig_len = config_data.len() + multisig_config.threshold() as usize * 65; - let mut omni_sig = vec![0u8; multisig_len]; - omni_sig[..config_data.len()].copy_from_slice(&config_data); - OmniLockWitnessLock::new_builder().signature(Some(Bytes::from(omni_sig)).pack()) + let multisig_len = + config_data.len() + multisig_config.threshold() as usize * SECP_SIGNATURE_SIZE; + // let mut omni_sig = vec![0u8; multisig_len]; + // omni_sig[..config_data.len()].copy_from_slice(&config_data); + // buf.extend_from_slice(&omni_sig); + let offset = buf.len(); + buf.extend_from_slice(&config_data); + buf.resize(offset + multisig_len, 0u8); + OmniLockWitnessLock::new_builder().signature(Some(buf.freeze()).pack()) } IdentityFlag::OwnerLock => OmniLockWitnessLock::new_builder(), _ => todo!("to support other placeholder_witness_lock implementions"), diff --git a/src/unlock/opentx/assembler.rs b/src/unlock/opentx/assembler.rs new file mode 100644 index 00000000..b0d02eec --- /dev/null +++ b/src/unlock/opentx/assembler.rs @@ -0,0 +1,130 @@ +use anyhow::anyhow; +use std::{cmp::Ordering, collections::HashSet, convert::TryFrom}; + +use ckb_types::{ + bytes::Bytes, + core::TransactionView, + packed::{Byte32, WitnessArgs}, + prelude::*, +}; + +use crate::{traits::TransactionDependencyProvider, unlock::omni_lock::OmniLockFlags}; +use crate::{ + tx_builder::{gen_script_groups, ScriptGroups}, + types::omni_lock::OmniLockWitnessLock, +}; + +use super::OpenTxError; + +/// Check if different +fn check_script_groups(group_vec: &[ScriptGroups]) -> Result<(), OpenTxError> { + let mut keys = HashSet::new(); + for group in group_vec.iter() { + let len = keys.len(); + keys.extend(group.lock_groups.keys().clone()); + if len + group.lock_groups.len() > keys.len() { + return Err(OpenTxError::SameLockInDifferentOpenTx); + } + } + Ok(()) +} + +/// Assemble a transaction from multiple opentransaction, remove duplicate cell deps and header deps. +/// Alter base input/output index. +pub fn assemble_new_tx( + mut transactions: Vec, + provider: &dyn TransactionDependencyProvider, + opentx_code_hash: Byte32, +) -> Result { + if transactions.len() == 1 { + return Ok(transactions.remove(0)); + } + let mut builder = TransactionView::new_advanced_builder(); + let mut cell_deps = HashSet::new(); + let mut header_deps = HashSet::new(); + let mut base_input_idx = 0usize; + let mut base_output_idx = 0usize; + let mut base_input_cap = 0usize; + let mut base_output_cap = 0usize; + let group_vec: Result, _> = transactions + .iter() + .map(|tx| gen_script_groups(tx, provider)) + .collect(); + let group_vec = group_vec?; + check_script_groups(&group_vec)?; + for tx in transactions.iter() { + cell_deps.extend(tx.cell_deps()); + header_deps.extend(tx.header_deps()); + builder = builder.inputs(tx.inputs()); + base_input_cap += tx.inputs().len(); + base_output_cap += tx.outputs().len(); + // Handle opentx witness + for (input, witness) in tx.inputs().into_iter().zip(tx.witnesses().into_iter()) { + let lock = provider.get_cell(&input.previous_output())?.lock(); + let code_hash = lock.code_hash(); + // empty witness should be in a script group + if !witness.is_empty() && code_hash.cmp(&opentx_code_hash) == Ordering::Equal { + let args = &lock.args().raw_data(); + let witness_data = witness.raw_data(); + if witness_data.len() > 8 // sizeof base_input + sizeof base_output + && args.len() >= 22 + && OmniLockFlags::from_bits_truncate(args[21]).contains(OmniLockFlags::OPENTX) + { + // Parse lock data + let current_witness: WitnessArgs = + WitnessArgs::from_slice(witness_data.as_ref())?; + let lock_field = current_witness + .lock() + .to_opt() + .map(|data| data.raw_data()) + .ok_or(OpenTxError::WitnessLockMissing)?; + let omnilock_witnesslock = + OmniLockWitnessLock::from_slice(lock_field.as_ref())?; + + let mut data = omnilock_witnesslock + .signature() + .to_opt() + .map(|data| data.raw_data().as_ref().to_vec()) + .ok_or(OpenTxError::SignatureMissing)?; + + let mut tmp = [0u8; 4]; + tmp.copy_from_slice(&data[0..4]); + let this_base_input_idx = u32::from_le_bytes(tmp) + + u32::try_from(base_input_idx).map_err(|e| anyhow!(e))?; + if this_base_input_idx as usize > base_input_cap { + return Err(OpenTxError::BaseInputIndexOverFlow); + } + data[0..4].copy_from_slice(&this_base_input_idx.to_le_bytes()); + + tmp.copy_from_slice(&data[4..8]); + let this_base_output_idx = u32::from_le_bytes(tmp) + + u32::try_from(base_output_idx).map_err(|e| anyhow!(e))?; + if this_base_output_idx as usize > base_output_cap { + return Err(OpenTxError::BaseOutputIndexOverFlow); + } + data[4..8].copy_from_slice(&this_base_output_idx.to_le_bytes()); + + let omnilock_witnesslock = omnilock_witnesslock + .as_builder() + .signature(Some(Bytes::from(data)).pack()) + .build(); + let witness = current_witness + .as_builder() + .lock(Some(omnilock_witnesslock.as_bytes()).pack()) + .build(); + builder = builder.witness(witness.as_bytes().pack()); + continue; + } + } + builder = builder.witness(witness); + } + builder = builder.outputs(tx.outputs()); + builder = builder.outputs_data(tx.outputs_data()); + + base_input_idx += tx.inputs().len(); + base_output_idx += tx.outputs().len(); + } + builder = builder.cell_deps(cell_deps).header_deps(header_deps); + + Ok(builder.build()) +} diff --git a/src/unlock/opentx/hasher.rs b/src/unlock/opentx/hasher.rs new file mode 100644 index 00000000..7d986d14 --- /dev/null +++ b/src/unlock/opentx/hasher.rs @@ -0,0 +1,716 @@ +use anyhow::anyhow; +use bitflags::bitflags; +use bytes::{BufMut, Bytes}; +use ckb_hash::Blake2b; +use ckb_types::{bytes::BytesMut, core::TransactionView, prelude::*}; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +use enum_repr_derive::{FromEnumToRepr, TryFromReprToEnum}; + +use super::reader::{OpenTxCellField, OpenTxReader, OpenTxSource}; +use super::OpenTxError; + +const ARG1_MASK: u16 = 0xFFF; +const ARG2_MASK: u16 = 0xFFF; + +/// Open transaction signature input command. +#[derive( + Clone, + Copy, + Serialize, + Deserialize, + Debug, + Hash, + Eq, + PartialEq, + TryFromReprToEnum, + FromEnumToRepr, +)] +#[repr(u8)] +pub enum OpentxCommand { + /// Hash the full current transaction hash + TxHash = 0x00, + /// Hash length of input & output cells in current script group + GroupInputOutputLen = 0x01, + /// Hash part or the whole output cell. arg1 is index of output cell, arg2 is cell mask. + IndexOutput = 0x11, + /// Hash part or the whole output cell. arg1 is offset of output cell, arg2 is cell mask. + OffsetOutput = 0x12, + /// Hash part or the whole input cell. arg1 is index of input cell, arg2 is cell mask. + IndexInput = 0x13, + /// Hash part or the whole input cell. arg1 is offset of input cell, arg2 is cell mask. + OffsetInput = 0x14, + /// Hash part or the whole cell input structure, arg1 is index of input cell, arg2 is input mask. + CellInputIndex = 0x15, + /// Hash part or the whole cell input structure, arg1 is offset of input cell, arg2 is input mask.` + CellInputOffset = 0x16, + /// Concatenate ARG 1 and ARG 2, arg1 is lower 12 bit, arg2 is higher 12 bit. + /// The purpose of this command is to add salt for hash. + ConcatArg1Arg2 = 0x20, + /// Terminate and generate the final blake2b hash + End = 0xF0, +} + +impl OpentxCommand { + pub fn is_index(&self) -> bool { + matches!( + self, + OpentxCommand::IndexOutput | OpentxCommand::IndexInput | OpentxCommand::CellInputIndex + ) + } +} + +bitflags! { + /// The bits control the data to generate from a cell. + #[derive(Serialize, Deserialize)] + pub struct CellMask: u16 { + /// capacity + const CAPACITY = 0x1; + /// lock.code_hash + const LOCK_CODE_HASH = 0x2; + /// lock.hash_type + const LOCK_HASH_TYPE = 0x4; + /// lock.args + const LOCK_ARGS = 0x8; + /// type.code_hash + const TYPE_CODE_HASH = 0x10; + /// type.hash_type + const TYPE_HASH_TYPE = 0x20; + /// type.args + const TYPE_ARGS = 0x40; + /// Cell data + const CELL_DATA = 0x80; + /// Lock script hash + const LOCK_SCRIPT_HASH = 0x100; + /// Type script hash + const TYPE_SCRIPT_HASH = 0x200; + /// The whole cell + const WHOLE_CELL = 0x400; + } +} + +bitflags! { + /// The bits control the data to generate from a CellInput structure. + #[derive(Serialize, Deserialize)] + pub struct InputMask: u16 { + /// previous_output.tx_hash + const TX_HASH = 0x1; + /// previous_output.index + const INDEX = 0x2; + /// since + const SINCE = 0x4; + /// previous_output + const PREVIOUS_OUTPUT = 0x8; + /// The whole CellInput structure + const WHOLE = 0x10; + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq)] +pub struct OpenTxSigInput { + pub cmd: OpentxCommand, + pub arg1: u16, + pub arg2: u16, +} + +impl OpenTxSigInput { + pub fn compose(&self) -> u32 { + (self.cmd as u32) + + (((self.arg1 & ARG1_MASK) as u32) << 8) + + (((self.arg2 & ARG2_MASK) as u32) << 20) + } + + /// Build new OpentxCommand::TxHash OpenTxSigInput, command 0x00 + pub fn new_tx_hash() -> OpenTxSigInput { + OpenTxSigInput { + cmd: OpentxCommand::TxHash, + arg1: 0, + arg2: 0, + } + } + // Build new OpentxCommand::GroupInputOutputLen OpenTxSigInput, command 0x01 + pub fn new_group_input_output_len() -> OpenTxSigInput { + OpenTxSigInput { + cmd: OpentxCommand::GroupInputOutputLen, + arg1: 0, + arg2: 0, + } + } + /// Build new OpentxCommand::IndexOutput OpenTxSigInput, command 0x11 + pub fn new_index_output(arg1: u16, arg2: CellMask) -> Result { + Self::new_cell_command(OpentxCommand::IndexOutput, arg1, arg2) + } + /// Build new OpentxCommand::OffsetOutput OpenTxSigInput, command 0x12 + pub fn new_offset_output(arg1: u16, arg2: CellMask) -> Result { + Self::new_cell_command(OpentxCommand::OffsetOutput, arg1, arg2) + } + /// Build new OpentxCommand::IndexInput OpenTxSigInput, command 0x13 + pub fn new_index_input(arg1: u16, arg2: CellMask) -> Result { + Self::new_cell_command(OpentxCommand::IndexInput, arg1, arg2) + } + /// Build new OpentxCommand::OffsetInput OpenTxSigInput, command 0x14 + pub fn new_offset_input(arg1: u16, arg2: CellMask) -> Result { + Self::new_cell_command(OpentxCommand::OffsetInput, arg1, arg2) + } + /// Build new OpenTxSigInput to handle part or the whole input/output cell + pub fn new_cell_command( + cmd: OpentxCommand, + arg1: u16, + arg2: CellMask, + ) -> Result { + if arg1 > ARG1_MASK { + return Err(OpenTxError::Arg1OutOfRange(arg1)); + } + + Ok(OpenTxSigInput { + cmd, + arg1, + arg2: arg2.bits, + }) + } + /// Build new OpentxCommand::CellInputIndex OpenTxSigInput, command 0x15 + pub fn new_cell_input_index(arg1: u16, arg2: InputMask) -> Result { + Self::new_input_command(OpentxCommand::CellInputIndex, arg1, arg2) + } + //// Build new OpentxCommand::CellInputOffset OpenTxSigInput, command 0x16 + pub fn new_cell_input_offset( + arg1: u16, + arg2: InputMask, + ) -> Result { + Self::new_input_command(OpentxCommand::CellInputOffset, arg1, arg2) + } + /// Build new OpenTxSigInput to hash part or the whole cell input structure + pub fn new_input_command( + cmd: OpentxCommand, + arg1: u16, + arg2: InputMask, + ) -> Result { + if arg1 > ARG1_MASK { + return Err(OpenTxError::Arg1OutOfRange(arg1)); + } + + Ok(OpenTxSigInput { + cmd, + arg1, + arg2: arg2.bits, + }) + } + + /// Build new OpentxCommand::ConcatArg1Arg2 OpenTxSigInput, command 0x20 + pub fn new_concat_arg1_arg2(arg1: u16, arg2: u16) -> OpenTxSigInput { + OpenTxSigInput { + cmd: OpentxCommand::ConcatArg1Arg2, + arg1: arg1 & ARG1_MASK, + arg2: arg2 & ARG2_MASK, + } + } + /// Build new OpentxCommand::End OpenTxSigInput, command 0xF0 + pub fn new_end() -> OpenTxSigInput { + OpenTxSigInput { + cmd: OpentxCommand::End, + arg1: 0, + arg2: 0, + } + } + fn hash_cell( + &self, + cache: &mut OpentxCache, + reader: &OpenTxReader, + is_input: bool, + with_offset: bool, + base_index: u32, + ) -> Result<(), OpenTxError> { + let mut index = self.arg1 as usize; + if with_offset { + index += base_index as usize; + } + let source = if is_input { + OpenTxSource::Input + } else { + OpenTxSource::Outpout + }; + let cell_mask = CellMask::from_bits_truncate(self.arg2); + if cell_mask.contains(CellMask::CAPACITY) { + let data = reader.load_cell_field(index, source, OpenTxCellField::Capacity)?; + cache.update(&data); + } + if cell_mask.intersects( + CellMask::LOCK_CODE_HASH + | CellMask::LOCK_HASH_TYPE + | CellMask::LOCK_ARGS + | CellMask::TYPE_CODE_HASH + | CellMask::TYPE_HASH_TYPE + | CellMask::TYPE_ARGS, + ) { + let cell = reader.get_cell(index, is_input)?; + let lock = cell.lock(); + if cell_mask.contains(CellMask::LOCK_CODE_HASH) { + cache.update(lock.code_hash().as_slice()); + } + if cell_mask.contains(CellMask::LOCK_HASH_TYPE) { + cache.update(lock.hash_type().as_slice()); + } + if cell_mask.contains(CellMask::LOCK_ARGS) { + let args = lock.args().raw_data().to_vec(); + cache.update(args.as_slice()); + } + + if let Some(type_) = cell.type_().to_opt() { + if cell_mask.contains(CellMask::TYPE_CODE_HASH) { + cache.update(type_.code_hash().as_slice()); + } + if cell_mask.contains(CellMask::TYPE_HASH_TYPE) { + cache.update(type_.hash_type().as_slice()); + } + if cell_mask.contains(CellMask::TYPE_ARGS) { + let args = type_.args().raw_data().to_vec(); + cache.update(&args); + } + } + } + if cell_mask.contains(CellMask::CELL_DATA) { + let data = reader.load_cell_data(index, source)?; + cache.update(data.as_slice()); + } + + if cell_mask.contains(CellMask::LOCK_SCRIPT_HASH) { + let cell = reader.get_cell(index, is_input)?; + let hash = cell.lock().calc_script_hash(); + cache.update(hash.as_slice()); + } + + if cell_mask.contains(CellMask::TYPE_SCRIPT_HASH) { + let cell = reader.get_cell(index, is_input)?; + if let Some(script) = cell.type_().to_opt() { + let hash = script.calc_script_hash(); + cache.update(hash.as_slice()); + } + } + + if cell_mask.contains(CellMask::WHOLE_CELL) { + let data = reader.load_cell(index, source)?; + cache.update(data.as_slice()); + } + Result::Ok(()) + } + + fn hash_input( + &self, + cache: &mut OpentxCache, + reader: &OpenTxReader, + with_offset: bool, + base_index: u32, + ) -> Result<(), OpenTxError> { + let index = if with_offset { + usize::try_from(base_index) + .map_err(|e| anyhow!(e))? + .checked_add(self.arg1 as usize) + .ok_or_else(|| anyhow!("add {} and {} overflow", base_index, self.arg1))? + } else { + self.arg1 as usize + }; + + let input_mask = InputMask::from_bits_truncate(self.arg2); + if input_mask.contains(InputMask::TX_HASH) { + let cell = reader.input(index)?; + let data = cell.previous_output().tx_hash(); + cache.update(data.as_slice()); + } + + if input_mask.contains(InputMask::INDEX) { + let cell = reader.input(index)?; + let data = cell.previous_output().index(); + cache.update(data.as_slice()); + } + + if input_mask.contains(InputMask::SINCE) { + let data = reader.load_input_field_since(index)?; + + cache.update(&data); + } + + if input_mask.contains(InputMask::PREVIOUS_OUTPUT) { + let data = reader.load_input_field_out_point(index)?; + + cache.update(&data); + } + + if input_mask.contains(InputMask::WHOLE) { + let data = reader.load_input(index)?; + cache.update(&data); + } + Ok(()) + } +} +#[derive(Clone, Serialize, Deserialize, Debug, Hash, Eq, PartialEq)] +pub struct OpentxWitness { + pub base_input_index: u32, + pub base_output_index: u32, + pub inputs: Vec, +} + +impl OpentxWitness { + pub fn new_empty() -> Self { + OpentxWitness { + base_input_index: 0, + base_output_index: 0, + inputs: vec![], + } + } + pub fn new(base_input_index: u32, base_output_index: u32, inputs: Vec) -> Self { + OpentxWitness { + base_input_index, + base_output_index, + inputs, + } + } + + /// Build new OpentxWitness which will sign all data. + /// + /// It will first generate the GroupInputOutputLen(0x01), + /// then iterate the inputs to generate the relative index OpenTxSigInput with all CellMask on, and InputMask on, + /// then iterate each output to generate the relative index OpenTxSigInput with all CellMask on. + /// + /// If salt provided, it will add low 24 bits with ConcatArg1Arg2(0x20), + /// if high 8 bits not all 0, it will add another ConcatArg1Arg2 OpenTxSigInput. + /// + /// Then it will add an End(0xF0) OpenTxSigInput. + /// + /// The range of inputs/outputs are [base_input_index, end_input_index), [base_output_index, end_output_index). + /// If end_input_index bigger than the transaction inputs length, the inputs length will be used, same thing to end_output_index. + /// + pub fn new_sig_range_relative( + transaction: &TransactionView, + salt: Option, + base_input_index: usize, + end_input_index: usize, + base_output_index: usize, + end_output_index: usize, + ) -> Result { + let mut inputs = vec![ + // If the opentx will add other input/output, when sign, it can not get the correct transaction hash, + // so OpentxCommand::TxHash should not be included in an opentx will add more input(s)/output(s) + // OpenTxSigInput::new_tx_hash(), + OpenTxSigInput::new_group_input_output_len(), + ]; + + let start_input_idx = base_input_index; + let end_input_idx = end_input_index.min(transaction.inputs().len()); + if start_input_idx >= end_input_idx { + return Err(OpenTxError::BaseIndexOverFlow( + start_input_idx, + end_input_idx, + )); + } + + let start_output_idx = base_output_index; + let out_put_idx = end_output_index.min(transaction.outputs().len()); + if start_output_idx >= out_put_idx { + return Err(OpenTxError::BaseIndexOverFlow( + start_output_idx, + out_put_idx, + )); + } + let base_input_index = u32::try_from(base_input_index).map_err(|e| anyhow!(e))?; + let base_output_index = u32::try_from(base_output_index).map_err(|e| anyhow!(e))?; + for input_idx in start_input_idx..end_input_idx { + let idx = u16::try_from(input_idx - start_input_idx).map_err(|e| anyhow!(e))?; + inputs.push(OpenTxSigInput::new_offset_input( + idx as u16, + CellMask::all(), + )?); + inputs.push(OpenTxSigInput::new_cell_input_offset( + idx as u16, + InputMask::all(), + )?); + } + for output_idx in start_output_idx..out_put_idx { + let idx = u16::try_from(output_idx - start_output_idx).map_err(|e| anyhow!(e))?; + inputs.push(OpenTxSigInput::new_offset_output( + idx as u16, + CellMask::all(), + )?); + } + if let Some(mut salt) = salt { + while salt > 0 { + inputs.push(OpenTxSigInput::new_concat_arg1_arg2( + salt as u16 & ARG1_MASK, + (salt >> 12) as u16 & ARG2_MASK, + )); + salt >>= 24; + } + } + + inputs.push(OpenTxSigInput::new_end()); + Ok(OpentxWitness::new( + base_input_index, + base_output_index, + inputs, + )) + } + /// Same to `new_sig_range_relative`, except end_input_index and end_output_index are all usize::MAX, + /// which will be changed to the length of inputs/outputs list. + pub fn new_sig_to_end_relative( + transaction: &TransactionView, + salt: Option, + base_input_index: usize, + base_output_index: usize, + ) -> Result { + Self::new_sig_range_relative( + transaction, + salt, + base_input_index, + usize::MAX, + base_output_index, + usize::MAX, + ) + } + + /// Same to `new_sig_to_end_relative`, except base_input_index and base_output_index are all 0 + pub fn new_sig_all_relative( + transaction: &TransactionView, + salt: Option, + ) -> Result { + Self::new_sig_range_relative(transaction, salt, 0, usize::MAX, 0, usize::MAX) + } + + /// Same to new_sig_to_end_relative, but the will use the index commands. + /// The length will be limit to 0x1000, index range:[0, 4095]. + pub fn new_sig_range_absolute( + transaction: &TransactionView, + salt: Option, + base_input_index: usize, + end_input_index: usize, + base_output_index: usize, + end_output_index: usize, + ) -> Result { + let mut inputs = vec![OpenTxSigInput::new_group_input_output_len()]; + + let start_input_idx = base_input_index; + let end_input_idx = + ((ARG1_MASK + 1) as usize).min(end_input_index.min(transaction.inputs().len())); + if start_input_idx >= end_input_idx { + return Err(OpenTxError::BaseIndexOverFlow( + start_input_idx, + end_input_idx, + )); + } + + let start_output_idx = base_output_index; + let out_put_idx = + ((ARG1_MASK + 1) as usize).min(end_output_index.min(transaction.outputs().len())); + if start_output_idx >= out_put_idx { + return Err(OpenTxError::BaseIndexOverFlow( + start_output_idx, + out_put_idx, + )); + } + let base_input_index = u32::try_from(base_input_index).map_err(|e| anyhow!(e))?; + let base_output_index = u32::try_from(base_output_index).map_err(|e| anyhow!(e))?; + for input_idx in start_input_idx..end_input_idx { + inputs.push(OpenTxSigInput::new_index_input( + input_idx as u16, + CellMask::all(), + )?); + inputs.push(OpenTxSigInput::new_cell_input_index( + input_idx as u16, + InputMask::all(), + )?); + } + for output_idx in start_output_idx..out_put_idx { + inputs.push(OpenTxSigInput::new_index_output( + output_idx as u16, + CellMask::all(), + )?); + } + if let Some(mut salt) = salt { + while salt > 0 { + inputs.push(OpenTxSigInput::new_concat_arg1_arg2( + salt as u16 & ARG1_MASK, + (salt >> 12) as u16 & ARG2_MASK, + )); + salt >>= 24; + } + } + + inputs.push(OpenTxSigInput::new_end()); + Ok(OpentxWitness::new( + base_input_index, + base_output_index, + inputs, + )) + } + /// Same to `new_sig_range_absolute`, except end_input_index and end_output_index are all usize::MAX, + /// which will be changed to the length of inputs/outputs list. + pub fn new_sig_to_end_absolute( + transaction: &TransactionView, + salt: Option, + base_input_index: usize, + base_output_index: usize, + ) -> Result { + Self::new_sig_range_absolute( + transaction, + salt, + base_input_index, + usize::MAX, + base_output_index, + usize::MAX, + ) + } + + /// Same to `new_sig_to_end_absolute`, except base_input_index and base_output_index are all 0 + pub fn new_sig_all_absolute( + transaction: &TransactionView, + salt: Option, + ) -> Result { + Self::new_sig_range_absolute(transaction, salt, 0, usize::MAX, 0, usize::MAX) + } + + /// Add OpentxCommand to the first of the input list, this should only be called if the opentx is ready to sent. + /// If the open transaction will be add input/output, this function should not be called. + pub fn add_tx_hash_input(&mut self) { + self.inputs.insert(0, OpenTxSigInput::new_tx_hash()); + } + + pub fn set_base_input_index(&mut self, index: u32) { + self.base_input_index = index; + } + + pub fn set_base_output_index(&mut self, index: u32) { + self.base_output_index = index; + } + + pub fn to_witness_data(&self) -> Vec { + let capacity = self.opentx_sig_data_len(); + let mut witness_data = Vec::with_capacity(capacity); + witness_data.extend_from_slice(&self.base_input_index.to_le_bytes()); + witness_data.extend_from_slice(&self.base_output_index.to_le_bytes()); + for inpt in &self.inputs { + witness_data.extend_from_slice(&inpt.compose().to_le_bytes()); + } + witness_data + } + + /// Generate message for sign. + /// + /// # Arguments + /// * `reader` the read object can fetch data from blockchain. + /// + /// Return a tuple with first one is the message, the second is open transaction data exclude signature in witness. + pub fn generate_message(&self, reader: &OpenTxReader) -> Result<(Bytes, Bytes), OpenTxError> { + let (is_input, is_output) = (true, false); + let (relative_idx, absolute_idx) = (true, false); + + let mut cache = OpentxCache::new(); + let mut s_data = BytesMut::with_capacity(self.opentx_sig_data_len()); + s_data.put_u32_le(self.base_input_index as u32); + s_data.put_u32_le(self.base_output_index as u32); + let mut has_last = false; + for si in &self.inputs { + match si.cmd { + OpentxCommand::TxHash => { + let tx_hash = reader.tx_hash(); + cache.update(tx_hash.as_slice()); + } + OpentxCommand::GroupInputOutputLen => { + let input_len = reader.group_input_len()?; + cache.update(&input_len.to_le_bytes()); + let output_len = 0u64; + cache.update(&output_len.to_le_bytes()); + } + OpentxCommand::IndexOutput => { + si.hash_cell(&mut cache, reader, is_output, absolute_idx, 0)?; + } + OpentxCommand::OffsetOutput => { + si.hash_cell( + &mut cache, + reader, + is_output, + relative_idx, + self.base_output_index, + )?; + } + OpentxCommand::IndexInput => { + si.hash_cell(&mut cache, reader, is_input, absolute_idx, 0)?; + } + OpentxCommand::OffsetInput => { + si.hash_cell( + &mut cache, + reader, + is_input, + relative_idx, + self.base_input_index, + )?; + } + OpentxCommand::CellInputIndex => { + si.hash_input(&mut cache, reader, absolute_idx, 0)?; + } + OpentxCommand::CellInputOffset => { + si.hash_input(&mut cache, reader, is_input, self.base_input_index)?; + } + OpentxCommand::ConcatArg1Arg2 => { + let data = (si.arg1 as u32 & 0xfff) | ((si.arg2 as u32 & 0xfff) << 12); + let data = data.to_le_bytes(); + cache.update(&data[0..3]); + } + OpentxCommand::End => { + has_last = true; + s_data.extend_from_slice(&si.compose().to_le_bytes()); + break; + } + } + s_data.extend_from_slice(&si.compose().to_le_bytes()); + } + // append last end command + if !has_last { + let si = OpenTxSigInput::new_end(); + s_data.extend_from_slice(&si.compose().to_le_bytes()); + } + let s_data = s_data.freeze(); + cache.update(s_data[8..].to_vec().as_slice()); + + let msg = cache.finalize(); + Ok((msg, s_data)) + } + + /// The byte length of base_input_index, base_output_index, and signature input list in the witness field. + pub fn opentx_sig_data_len(&self) -> usize { + 4 + 4 + 4 * self.inputs.len() + } + + /// Build open transaction signature by combile open_sig_data and real signatures sig_bytes + /// + /// # Arguments + /// * `open_sig_data` open transaction data, include base_input_index, base_output_index, and signature input list. + /// * `sig_bytes` real signature bytes. + pub fn build_opentx_sig(&self, open_sig_data: Bytes, sig_bytes: Bytes) -> Bytes { + let mut data = BytesMut::with_capacity(self.opentx_sig_data_len() + sig_bytes.len()); + + data.put(open_sig_data); + data.put(sig_bytes); + data.freeze() + } +} + +struct OpentxCache { + blake2b: Blake2b, +} + +impl OpentxCache { + pub fn new() -> Self { + OpentxCache { + blake2b: ckb_hash::new_blake2b(), + } + } + + pub fn update(&mut self, data: &[u8]) { + self.blake2b.update(data); + } + + pub fn finalize(self) -> Bytes { + let mut msg = vec![0u8; 32]; + self.blake2b.finalize(&mut msg); + Bytes::from(msg) + } +} diff --git a/src/unlock/opentx/mod.rs b/src/unlock/opentx/mod.rs new file mode 100644 index 00000000..bcdb68c3 --- /dev/null +++ b/src/unlock/opentx/mod.rs @@ -0,0 +1,47 @@ +pub mod assembler; +pub mod hasher; +pub mod reader; + +use ckb_types::error::VerificationError; +pub use hasher::OpentxWitness; + +use crate::traits::TransactionDependencyError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum OpenTxError { + #[error("Transaction read error, index out of bound.")] + OutOfBound, + #[error("Item not exist")] + ItemMissing, + #[error("Fail to get cell `{0}`")] + CellNotExist(#[from] TransactionDependencyError), + #[error("Unsupport data source")] + UnsupportSource, + #[error("usize(`{0}`) to u64 overflow.")] + LenOverflow(usize), + + #[error("arg1(`{0}`) out of range")] + Arg1OutOfRange(u16), + #[error("arg2(`{0}`) out of range")] + Arg2OutOfRange(u16), + #[error("base index(`{0}) bigger than end index(`{1}`)")] + BaseIndexOverFlow(usize, usize), + + #[error(transparent)] + VerificationError(#[from] VerificationError), + #[error("Open transaction input list not configured.")] + InputListConfigMissing, + #[error("lock field of witness not exist")] + WitnessLockMissing, + #[error("signature not exist")] + SignatureMissing, + #[error("Cells in different partital open transaction can not be merged")] + SameLockInDifferentOpenTx, + #[error("Base input index is overflow")] + BaseInputIndexOverFlow, + #[error("Base output index is overflow")] + BaseOutputIndexOverFlow, + #[error(transparent)] + Other(#[from] anyhow::Error), +} diff --git a/src/unlock/opentx/reader.rs b/src/unlock/opentx/reader.rs new file mode 100644 index 00000000..baebcbe0 --- /dev/null +++ b/src/unlock/opentx/reader.rs @@ -0,0 +1,479 @@ +use std::convert::TryFrom; + +use ckb_hash::blake2b_256; +use ckb_types::{ + core::{Capacity, TransactionView}, + packed::{Byte32, CellDep, CellInput, CellOutput, OutPoint, Script}, + prelude::Entity, +}; + +use super::OpenTxError; +use crate::{traits::TransactionDependencyProvider, ScriptGroup}; + +#[derive(Copy, Clone, PartialEq)] +pub enum OpenTxSource { + Input, + GroupInput, + Outpout, + CellDep, +} + +#[derive(Copy, Clone)] +pub enum OpenTxCellField { + Capacity, + DataHash, + Lock, + LockHash, + Type, + TypeHash, + OccupiedCapacity, +} + +#[derive(Copy, Clone)] +pub enum OpenTxInputField { + OutPoint, + Since, +} + +/// This reader can only read for only one input group, if you have multiple input group, multiple reader will be needed. +pub struct OpenTxReader<'r> { + pub transaction: TransactionView, + pub provider: &'r dyn TransactionDependencyProvider, + /// map group input index to input index + group_input_index: Vec, +} + +impl<'r> OpenTxReader<'r> { + pub fn new( + transaction: &TransactionView, + provider: &'r dyn TransactionDependencyProvider, + script_group: &ScriptGroup, + ) -> Result { + Ok(OpenTxReader { + transaction: transaction.clone(), + provider, + group_input_index: script_group.input_indices.clone(), + }) + } + + /// get the group input length. + pub fn group_input_len(&self) -> Result { + let len = self.group_input_index.len(); + let len = u64::try_from(len).map_err(|_e| OpenTxError::LenOverflow(len))?; + Ok(len) + } + + /// Get input at absolute index + pub fn input(&self, index: usize) -> Result { + self.transaction + .inputs() + .get(index) + .ok_or(OpenTxError::OutOfBound) + } + /// Get previous output of input + /// # Arguments + /// * `index` absolute index of inputs. + fn input_previous_output(&self, index: usize) -> Result { + let cell = self.input(index)?; + Ok(cell.previous_output()) + } + + /// Get CellOutput of input's cell + /// # Arguments + /// * `index` absolute index of inputs. + fn input_cell(&self, index: usize) -> Result { + let previous_output = self.input_previous_output(index)?; + let cell_output = self.provider.get_cell(&previous_output)?; + Ok(cell_output) + } + + /// Get CellOutput of input's cell + /// # Arguments + /// * `index` absolute index of inputs. + fn group_input_cell(&self, index: usize) -> Result { + if self.group_input_index.len() <= index { + return Result::Err(OpenTxError::OutOfBound); + } + let index = self.group_input_index[index]; + self.input_cell(index) + } + /// Get cell data of input's cell + /// # Arguments + /// * `index` absolute index of inputs. + fn input_cell_data(&self, index: usize) -> Result { + let previous_output = self.input_previous_output(index)?; + let cell_data = self.provider.get_cell_data(&previous_output)?; + Ok(cell_data) + } + /// Get cell data of input's cell + /// # Arguments + /// * `index` absolute index of output. + fn output_cell(&self, index: usize) -> Result { + self.transaction + .output(index) + .ok_or(OpenTxError::OutOfBound) + } + /// Get cell raw data of output's cell + /// # Arguments + /// * `index` absolute index of output. + fn output_cell_data(&self, index: usize) -> Result { + Ok(self + .transaction + .outputs_data() + .get(index) + .ok_or(OpenTxError::OutOfBound)? + .raw_data()) + } + /// Get CellDep of cell depends + /// # Arguments + /// * `index` absolute index of cell deps. + fn cell_dep(&self, index: usize) -> Result { + self.transaction + .cell_deps() + .get(index) + .ok_or(OpenTxError::OutOfBound) + } + /// Get CellOutput of cell depend + /// # Arguments + /// * `index` absolute index of cell deps. + fn cell_dep_cell(&self, index: usize) -> Result { + let outpoint = self.cell_dep(index)?; + let cell = self.provider.get_cell(&outpoint.out_point())?; + Ok(cell) + } + fn cell_dep_cell_data(&self, index: usize) -> Result { + let outpoint = self.cell_dep(index)?; + let cell = self.provider.get_cell_data(&outpoint.out_point())?; + + Ok(cell) + } + /// Fetch the hash of the current running transaction + pub fn tx_hash(&self) -> Byte32 { + self.transaction.hash() + } + + pub fn load_transaction(&self) -> Vec { + self.transaction.data().as_slice().to_vec() + } + + pub fn load_cell(&self, index: usize, source: OpenTxSource) -> Result, OpenTxError> { + let cell = match source { + OpenTxSource::Input => self.input_cell(index), + OpenTxSource::Outpout => self.output_cell(index), + OpenTxSource::CellDep => self.cell_dep_cell(index), + _ => Err(OpenTxError::UnsupportSource), + }; + Ok(cell?.as_slice().to_vec()) + } + + pub fn load_cell_data( + &self, + index: usize, + source: OpenTxSource, + ) -> Result, OpenTxError> { + let data = match source { + OpenTxSource::Input => self.input_cell_data(index)?.to_vec(), + OpenTxSource::Outpout => self.output_cell_data(index)?.to_vec(), + OpenTxSource::CellDep => self.cell_dep_cell_data(index)?.to_vec(), + _ => return Err(OpenTxError::UnsupportSource), + }; + Ok(data.to_vec()) + } + + pub fn load_input(&self, index: usize) -> Result, OpenTxError> { + let input = self.input(index)?; + Result::Ok(input.as_slice().to_vec()) + } + + fn load_field_capacity( + &self, + index: usize, + source: OpenTxSource, + ) -> Result, OpenTxError> { + let cell = match source { + OpenTxSource::Input => self.input_cell(index)?, + OpenTxSource::Outpout => self.output_cell(index)?, + OpenTxSource::GroupInput => self.group_input_cell(index)?, + OpenTxSource::CellDep => self.cell_dep_cell(index)?, + }; + Ok(cell.capacity().raw_data().to_vec()) + } + + fn load_field_data_hash( + &self, + index: usize, + source: OpenTxSource, + ) -> Result, OpenTxError> { + match source { + OpenTxSource::Input => { + let input = self.input_cell_data(index)?; + + let data = input.to_vec(); + Result::Ok(if data.is_empty() { + [0u8; 32].to_vec() + } else { + blake2b_256(data).to_vec() + }) + } + OpenTxSource::Outpout => { + let output = self + .transaction + .outputs_data() + .get(index) + .ok_or(OpenTxError::OutOfBound)?; + let data = output.raw_data().to_vec(); + if data.is_empty() { + Result::Ok([0u8; 32].to_vec()) + } else { + Result::Ok(data) + } + } + OpenTxSource::CellDep => { + let outpoint = self.transaction.cell_deps().get(index); + if outpoint.is_none() { + return Result::Err(OpenTxError::OutOfBound); + } + let data = self + .provider + .get_cell_data(&outpoint.unwrap().out_point())?; + Result::Ok(if data.is_empty() { + [0u8; 32].to_vec() + } else { + blake2b_256(&data).to_vec() + }) + } + _ => Err(OpenTxError::UnsupportSource), + } + } + + fn load_field_lock(&self, index: usize, source: OpenTxSource) -> Result, OpenTxError> { + match source { + OpenTxSource::Input => { + let input = self.input_cell(index)?; + Result::Ok(input.lock().as_bytes().to_vec()) + } + OpenTxSource::Outpout => { + let output = self + .transaction + .output(index) + .ok_or(OpenTxError::OutOfBound)?; + Result::Ok(output.lock().as_bytes().to_vec()) + } + OpenTxSource::CellDep => { + let outpoint = self + .transaction + .cell_deps() + .get(index) + .ok_or(OpenTxError::OutOfBound)?; + let cell = self.provider.get_cell(&outpoint.out_point())?; + Result::Ok(cell.lock().as_bytes().to_vec()) + } + _ => Err(OpenTxError::UnsupportSource), + } + } + + fn load_field_lock_hash( + &self, + index: usize, + source: OpenTxSource, + ) -> Result, OpenTxError> { + match source { + OpenTxSource::Input => { + let input = self.input_cell(index)?; + Result::Ok(input.calc_lock_hash().as_bytes().to_vec()) + } + OpenTxSource::Outpout => { + let output = self + .transaction + .output(index) + .ok_or(OpenTxError::OutOfBound)?; + + Result::Ok(output.calc_lock_hash().as_bytes().to_vec()) + } + OpenTxSource::CellDep => { + let outpoint = self + .transaction + .cell_deps() + .get(index) + .ok_or(OpenTxError::OutOfBound)?; + + let cell = self.provider.get_cell(&outpoint.out_point())?; + Result::Ok(cell.calc_lock_hash().as_bytes().to_vec()) + } + _ => Err(OpenTxError::UnsupportSource), + } + } + + fn load_field_type(&self, index: usize, source: OpenTxSource) -> Result, OpenTxError> { + match source { + OpenTxSource::Input => { + let input = self.input_cell(index)?; + let d = input.type_(); + if d.is_none() { + Result::Err(OpenTxError::ItemMissing) + } else { + Result::Ok(d.as_bytes().to_vec()) + } + } + OpenTxSource::Outpout => { + let output = self + .transaction + .output(index) + .ok_or(OpenTxError::OutOfBound)?; + + let d = output.type_(); + if d.is_none() { + Result::Err(OpenTxError::ItemMissing) + } else { + Result::Ok(d.as_bytes().to_vec()) + } + } + OpenTxSource::CellDep => { + let outpoint = self + .transaction + .cell_deps() + .get(index) + .ok_or(OpenTxError::OutOfBound)?; + + let cell = self.provider.get_cell(&outpoint.out_point())?; + let d = cell.type_(); + if d.is_none() { + Result::Err(OpenTxError::ItemMissing) + } else { + Result::Ok(d.as_bytes().to_vec()) + } + } + _ => Err(OpenTxError::UnsupportSource), + } + } + + fn load_field_type_hash( + &self, + index: usize, + source: OpenTxSource, + ) -> Result, OpenTxError> { + match source { + OpenTxSource::Input => { + let input = self.input_cell(index)?; + let d = input.type_(); + if d.is_none() { + Result::Err(OpenTxError::ItemMissing) + } else { + let d = Script::from_slice(d.as_slice()).unwrap(); + Result::Ok(d.calc_script_hash().as_slice().to_vec()) + } + } + OpenTxSource::Outpout => { + let output = self + .transaction + .output(index) + .ok_or(OpenTxError::OutOfBound)?; + + let d = output.type_().to_opt().ok_or(OpenTxError::ItemMissing)?; + + Result::Ok(d.calc_script_hash().as_slice().to_vec()) + } + OpenTxSource::CellDep => { + let outpoint = self.transaction.cell_deps().get(index); + if outpoint.is_none() { + return Result::Err(OpenTxError::OutOfBound); + } + let cell = self.provider.get_cell(&outpoint.unwrap().out_point())?; + let d = cell.type_().to_opt().ok_or(OpenTxError::ItemMissing)?; + + Result::Ok(d.calc_script_hash().as_slice().to_vec()) + } + _ => Err(OpenTxError::UnsupportSource), + } + } + + fn load_field_occupied_capacity( + &self, + index: usize, + source: OpenTxSource, + ) -> Result, OpenTxError> { + match source { + OpenTxSource::Input => { + let input = self.input_cell(index)?; + let data = self.input_cell_data(index)?; + Result::Ok( + input + .occupied_capacity(Capacity::bytes(data.len()).unwrap()) + .unwrap() + .as_u64() + .to_le_bytes() + .to_vec(), + ) + } + OpenTxSource::Outpout => { + let output = self.output_cell(index)?; + let output_data = self + .transaction + .outputs_data() + .get(index) + .ok_or(OpenTxError::OutOfBound)?; + + Result::Ok( + output + .occupied_capacity(Capacity::bytes(output_data.len()).unwrap()) + .unwrap() + .as_u64() + .to_le_bytes() + .to_vec(), + ) + } + OpenTxSource::CellDep => { + let cell = self + .transaction + .cell_deps() + .get(index) + .ok_or(OpenTxError::OutOfBound)?; + let cell_output = self.provider.get_cell(&cell.out_point())?; + let cell_data = self.provider.get_cell_data(&cell.out_point())?; + Result::Ok( + cell_output + .occupied_capacity(Capacity::bytes(cell_data.len()).unwrap()) + .unwrap() + .as_u64() + .to_le_bytes() + .to_vec(), + ) + } + _ => Err(OpenTxError::UnsupportSource), + } + } + + pub fn load_cell_field( + &self, + index: usize, + source: OpenTxSource, + field: OpenTxCellField, + ) -> Result, OpenTxError> { + match field { + OpenTxCellField::Capacity => self.load_field_capacity(index, source), + OpenTxCellField::DataHash => self.load_field_data_hash(index, source), + OpenTxCellField::Lock => self.load_field_lock(index, source), + OpenTxCellField::LockHash => self.load_field_lock_hash(index, source), + OpenTxCellField::Type => self.load_field_type(index, source), + OpenTxCellField::TypeHash => self.load_field_type_hash(index, source), + OpenTxCellField::OccupiedCapacity => self.load_field_occupied_capacity(index, source), + } + } + + pub fn load_input_field_out_point(&self, index: usize) -> Result, OpenTxError> { + Ok(self.input(index)?.previous_output().as_slice().to_vec()) + } + + pub fn load_input_field_since(&self, index: usize) -> Result, OpenTxError> { + Ok(self.input(index)?.since().as_slice().to_vec()) + } + + pub fn get_cell(&self, index: usize, is_input: bool) -> Result { + let cell = if is_input { + self.input_cell(index)? + } else { + self.output_cell(index)? + }; + Ok(cell) + } +} diff --git a/src/unlock/signer.rs b/src/unlock/signer.rs index a7da222e..b5a8c66d 100644 --- a/src/unlock/signer.rs +++ b/src/unlock/signer.rs @@ -13,8 +13,11 @@ use ckb_types::{ use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::types::{AddressPayload, CodeHashIndex, ScriptGroup, Since}; use crate::{constants::MULTISIG_TYPE_HASH, types::omni_lock::OmniLockWitnessLock}; +use crate::{ + traits::TransactionDependencyProvider, + types::{AddressPayload, CodeHashIndex, ScriptGroup, Since}, +}; use crate::{ traits::{Signer, SignerError}, util::convert_keccak256_hash, @@ -22,6 +25,7 @@ use crate::{ use super::{ omni_lock::{ConfigError, Identity}, + opentx::{reader::OpenTxReader, OpenTxError}, IdentityFlag, OmniLockConfig, }; @@ -48,6 +52,8 @@ pub enum ScriptSignError { #[error("there is an configuration error: `{0}`")] InvalidConfig(#[from] ConfigError), + #[error("open transaction read error: `{0}`")] + OpenTxError(#[from] OpenTxError), #[error(transparent)] Other(#[from] anyhow::Error), } @@ -64,6 +70,7 @@ pub trait ScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result; } @@ -128,6 +135,7 @@ impl ScriptSigner for SecpSighashScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let args = script_group.script.args().raw_data(); self.sign_tx_with_owner_id(args.as_ref(), tx, script_group) @@ -273,6 +281,7 @@ impl ScriptSigner for SecpMultisigScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let witness_idx = script_group.input_indices[0]; let mut witnesses: Vec = tx.witnesses().into_iter().collect(); @@ -365,6 +374,7 @@ impl ScriptSigner for AcpScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let args = script_group.script.args().raw_data(); let id = &args[0..20]; @@ -414,6 +424,7 @@ impl ScriptSigner for ChequeScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + _tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let args = script_group.script.args().raw_data(); let id = self.owner_id(args.as_ref()); @@ -537,6 +548,7 @@ impl OmniLockScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let witness_idx = script_group.input_indices[0]; let mut witnesses: Vec = tx.witnesses().into_iter().collect(); @@ -550,7 +562,21 @@ impl OmniLockScriptSigner { let zero_lock = self.config.zero_lock(self.unlock_mode)?; let zero_lock_len = zero_lock.len(); - let message = generate_message(&tx_new, script_group, zero_lock)?; + let (message, open_sig_data) = if self.config.is_opentx_mode() { + if let Some(opentx_wit) = self.config.get_opentx_input() { + let reader = OpenTxReader::new(&tx_new, tx_dep_provider, script_group)?; + opentx_wit.generate_message(&reader)? + } else { + return Err(ScriptSignError::OpenTxError( + OpenTxError::InputListConfigMissing, + )); + } + } else { + ( + generate_message(&tx_new, script_group, zero_lock)?, + Bytes::new(), + ) + }; let multisig_config = match self.unlock_mode { OmniUnlockMode::Admin => self @@ -589,23 +615,29 @@ impl OmniLockScriptSigner { OmniLockWitnessLock::default() }; let config_data = multisig_config.to_witness_data(); + let osdl = open_sig_data.len(); let mut omni_sig = omnilock_witnesslock .signature() .to_opt() .map(|data| data.raw_data().as_ref().to_vec()) .unwrap_or_else(|| { let mut omni_sig = - vec![0u8; config_data.len() + multisig_config.threshold() as usize * 65]; - omni_sig[..config_data.len()].copy_from_slice(&config_data); + vec![0u8; osdl + config_data.len() + multisig_config.threshold() as usize * 65]; + if osdl > 0 { + omni_sig[..osdl].copy_from_slice(&open_sig_data); + } + omni_sig[osdl..config_data.len()].copy_from_slice(&config_data); omni_sig }); for signature in signatures { - let mut idx = config_data.len(); + // every signature should start from begin in case one already exist. + let mut idx = osdl + config_data.len(); while idx < omni_sig.len() { - // Put signature into an empty place. + // signautrue already exist if omni_sig[idx..idx + 65] == signature { break; } else if omni_sig[idx..idx + 65] == [0u8; 65] { + // Put signature into an empty place. omni_sig[idx..idx + 65].copy_from_slice(signature.as_ref()); break; } @@ -631,6 +663,7 @@ impl OmniLockScriptSigner { tx: &TransactionView, script_group: &ScriptGroup, id: &Identity, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let witness_idx = script_group.input_indices[0]; let mut witnesses: Vec = tx.witnesses().into_iter().collect(); @@ -643,12 +676,20 @@ impl OmniLockScriptSigner { .build(); let zero_lock = self.config.zero_lock(self.unlock_mode())?; - let message = generate_message(&tx_new, script_group, zero_lock)?; + let (message, open_sig_data) = if let Some(opentx_wit) = self.config.get_opentx_input() { + let reader = OpenTxReader::new(&tx_new, tx_dep_provider, script_group)?; + opentx_wit.generate_message(&reader)? + } else { + ( + generate_message(&tx_new, script_group, zero_lock)?, + Bytes::new(), + ) + }; let message = convert_keccak256_hash(message.as_ref()); - let signature = self - .signer - .sign(id.auth_content().as_ref(), message.as_ref(), true, tx)?; + let mut signature = + self.signer + .sign(id.auth_content().as_ref(), message.as_ref(), true, tx)?; // Put signature into witness let witness_data = witnesses[witness_idx].raw_data(); @@ -657,7 +698,15 @@ impl OmniLockScriptSigner { } else { WitnessArgs::from_slice(witness_data.as_ref())? }; - + if self.config.is_opentx_mode() { + if let Some(opentx_wit) = self.config.get_opentx_input() { + signature = opentx_wit.build_opentx_sig(open_sig_data, signature); + } else { + return Err(ScriptSignError::OpenTxError( + OpenTxError::InputListConfigMissing, + )); + } + } let lock = Self::build_witness_lock(current_witness.lock(), signature)?; current_witness = current_witness.as_builder().lock(Some(lock).pack()).build(); witnesses[witness_idx] = current_witness.as_bytes().pack(); @@ -690,6 +739,10 @@ impl ScriptSigner for OmniLockScriptSigner { return false; } + if args != self.config.build_args() { + return false; + } + if self.unlock_mode == OmniUnlockMode::Admin { if let Some(admin_config) = self.config.get_admin_config() { if args.len() < 54 { @@ -742,6 +795,7 @@ impl ScriptSigner for OmniLockScriptSigner { &self, tx: &TransactionView, script_group: &ScriptGroup, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { let id = match self.unlock_mode { OmniUnlockMode::Admin => self @@ -765,9 +819,18 @@ impl ScriptSigner for OmniLockScriptSigner { .build(); let zero_lock = self.config.zero_lock(self.unlock_mode)?; - let message = generate_message(&tx_new, script_group, zero_lock)?; - - let signature = + let (message, open_sig_data) = + if let Some(opentx_wit) = self.config.get_opentx_input() { + let reader = OpenTxReader::new(&tx_new, tx_dep_provider, script_group)?; + opentx_wit.generate_message(&reader)? + } else { + ( + generate_message(&tx_new, script_group, zero_lock)?, + Bytes::new(), + ) + }; + + let mut signature = self.signer .sign(id.auth_content().as_ref(), message.as_ref(), true, tx)?; @@ -778,15 +841,23 @@ impl ScriptSigner for OmniLockScriptSigner { } else { WitnessArgs::from_slice(witness_data.as_ref())? }; - + if self.config.is_opentx_mode() { + if let Some(opentx_wit) = self.config.get_opentx_input() { + signature = opentx_wit.build_opentx_sig(open_sig_data, signature); + } else { + return Err(ScriptSignError::OpenTxError( + OpenTxError::InputListConfigMissing, + )); + } + } let lock = Self::build_witness_lock(current_witness.lock(), signature)?; current_witness = current_witness.as_builder().lock(Some(lock).pack()).build(); witnesses[witness_idx] = current_witness.as_bytes().pack(); Ok(tx.as_advanced_builder().set_witnesses(witnesses).build()) } - IdentityFlag::Ethereum => self.sign_ethereum_tx(tx, script_group, &id), - IdentityFlag::Multisig => self.sign_multisig_tx(tx, script_group), + IdentityFlag::Ethereum => self.sign_ethereum_tx(tx, script_group, &id, tx_dep_provider), + IdentityFlag::Multisig => self.sign_multisig_tx(tx, script_group, tx_dep_provider), IdentityFlag::OwnerLock => { // should not reach here, just return a clone for compatible reason. Ok(tx.clone()) diff --git a/src/unlock/unlocker.rs b/src/unlock/unlocker.rs index eccf417d..911c06ec 100644 --- a/src/unlock/unlocker.rs +++ b/src/unlock/unlocker.rs @@ -155,9 +155,9 @@ impl ScriptUnlocker for SecpSighashUnlocker { &self, tx: &TransactionView, script_group: &ScriptGroup, - _tx_dep_provider: &dyn TransactionDependencyProvider, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } fn fill_placeholder_witness( @@ -192,9 +192,9 @@ impl ScriptUnlocker for SecpMultisigUnlocker { &self, tx: &TransactionView, script_group: &ScriptGroup, - _tx_dep_provider: &dyn TransactionDependencyProvider, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } fn fill_placeholder_witness( @@ -429,7 +429,7 @@ impl ScriptUnlocker for AcpUnlocker { if self.is_unlocked(tx, script_group, tx_dep_provider)? { self.clear_placeholder_witness(tx, script_group) } else { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } } @@ -575,7 +575,7 @@ impl ScriptUnlocker for ChequeUnlocker { if self.is_unlocked(tx, script_group, tx_dep_provider)? { self.clear_placeholder_witness(tx, script_group) } else { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } } @@ -704,9 +704,9 @@ impl ScriptUnlocker for OmniLockUnlocker { &self, tx: &TransactionView, script_group: &ScriptGroup, - _tx_dep_provider: &dyn TransactionDependencyProvider, + tx_dep_provider: &dyn TransactionDependencyProvider, ) -> Result { - Ok(self.signer.sign_tx(tx, script_group)?) + Ok(self.signer.sign_tx(tx, script_group, tx_dep_provider)?) } fn fill_placeholder_witness(