参考文档

  • https://forum.qtum.org/topic/193/qtum%E6%98%9F%E7%81%AB%E7%BD%91%E7%BB%9C-sparknet-%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95%E5%8F%8A%E8%AF%B4%E6%98%8E
  • https://github.com/qtumproject/qtum/blob/testnet-1/doc/sparknet-guide.md
  • https://github.com/qtumproject/qtum/wiki/Qtum-Blockchain-Instruction
  • https://bodhiproject.github.io/wiki/deployment/
  • https://github.com/hayeah/qtum-dapp-counter#developing-the-dapp-ui

EVM对比 Qtum vs. Ethereum

  • Qtum的EVM来自Cpp Ethereum代码
  • Qtum基于UTXO模型,加入了账户抽象层(Account Abstraction Layer),用于将UTXO模型转换成可供EVM执行的账户模型。
  • Qtum对Bitcoin的Opcode进行扩展,添加了3个新的Opcode
    • OP_CREATE – 用于执行EVM智能合约的创建,把通过交易传输的字节代码存放到合约RLP数据库,并生成一个合约账户
    • OP_CALL – 用于传递调用智能合约所需要的相关数据(即EVM中的CALLERDATA)和地址信息,并执行合约中的代码内容。该操作符还可为智能合约发送资金。
    • OP_SPEND – 将当前合约的ID哈希值作为输入的交易HASH,或发送到合约的UTXO的交易HASH,然后使用OP_SPEND作为花费指令构建交易脚本。

Qtum Contract命令包括:

  • createcontract
  • callcontract ("query" mode, 所有的计算都是在链下(本地,local blockchain)进行,不需要消耗gas)
  • sendtocontract (“commit” mode, 所有的计算都是在链上进行的,并且所有的状态改变都会同步到链上。这个命令可以向合约发送代币,会消耗gas)
  • listcontracts

createcontract

$ ./src/qtum-cli -testnet help createcontract
createcontract "bytecode" (gaslimit gasprice "senderaddress" broadcast)
Create a contract with bytcode.

Arguments:
1. "bytecode"  (string, required) contract bytcode.
2. gasLimit  (numeric or string, optional) gasLimit, default: 2500000, max: 40000000
3. gasPrice  (numeric or string, optional) gasPrice QTUM price per gas unit, default: 0.0000004, min:0.0000004
4. "senderaddress" (string, optional) The quantum address that will be used to create the contract.
5. "broadcast" (bool, optional, default=true) Whether to broadcast the transaction or not.
6. "changeToSender" (bool, optional, default=true) Return the change to the sender.

Result:
[
  {
    "txid" : (string) The transaction id.
    "sender" : (string) QTUM address of the sender.
    "hash160" : (string) ripemd-160 hash of the sender.
    "address" : (string) expected contract address.
  }
]

Examples:
> qtum-cli createcontract "60606040525b33600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690836c010000000000000000000000009081020402179055506103786001600050819055505b600c80605b6000396
000f360606040526008565b600256"
> qtum-cli createcontract "60606040525b33600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690836c010000000000000000000000009081020402179055506103786001600050819055505b600c80605b6000396
000f360606040526008565b600256" 6000000 0.0000004 "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd" true

实现

https://github.com/qtumproject/qtum/blob/master/src/wallet/rpcwallet.cpp

    if(fBroadcast){
        CValidationState state;
        if (!pwalletMain->CommitTransaction(wtx, reservekey, g_connman.get(), state))
            throw JSONRPCError(RPC_WALLET_ERROR, "Error: The transaction was rejected! This might happen if some of the coins in your wallet were already spent, such as if you used a copy of the wallet and coins were spent in the copy but not marked as spent here.");

        std::string txId=wtx.GetHash().GetHex();
        result.push_back(Pair("txid", txId));

        CBitcoinAddress txSenderAdress(txSenderDest);
        CKeyID keyid;
        txSenderAdress.GetKeyID(keyid);

        result.push_back(Pair("sender", txSenderAdress.ToString()));
        result.push_back(Pair("hash160", HexStr(valtype(keyid.begin(),keyid.end()))));

        std::vector<unsigned char> SHA256TxVout(32);
        vector<unsigned char> contractAddress(20);
        vector<unsigned char> txIdAndVout(wtx.GetHash().begin(), wtx.GetHash().end());
        uint32_t voutNumber=0;
        BOOST_FOREACH(const CTxOut& txout, wtx.tx->vout) {
            if(txout.scriptPubKey.HasOpCreate()){
                std::vector<unsigned char> voutNumberChrs;
                if (voutNumberChrs.size() < sizeof(voutNumber))voutNumberChrs.resize(sizeof(voutNumber));
                std::memcpy(voutNumberChrs.data(), &voutNumber, sizeof(voutNumber));
                txIdAndVout.insert(txIdAndVout.end(),voutNumberChrs.begin(),voutNumberChrs.end());
                break;
            }
            voutNumber++;
        }
        CSHA256().Write(txIdAndVout.data(), txIdAndVout.size()).Finalize(SHA256TxVout.data());
        CRIPEMD160().Write(SHA256TxVout.data(), SHA256TxVout.size()).Finalize(contractAddress.data());
        result.push_back(Pair("address", HexStr(contractAddress)));
    }else{
        string strHex = EncodeHexTx(*wtx.tx, RPCSerializationFlags());
        result.push_back(Pair("raw transaction", strHex));
    }
    return result;

callcontract

$ ./src/qtum-cli -testnet help callcontract
callcontract "address" "data" ( address )

Argument:
1. "address"          (string, required) The account address
2. "data"             (string, required) The data hex string
3. address              (string, optional) The sender address hex string
4. gasLimit             (string, optional) The gas limit for executing the contract

sendtocontract

$ ./src/qtum-cli -testnet help sendtocontract
sendtocontract "contractaddress" "data" (amount gaslimit gasprice senderaddress broadcast)
Send funds and data to a contract.

Arguments:
1. "contractaddress" (string, required) The contract address that will receive the funds and data.
2. "datahex"  (string, required) data to send.
3. "amount"      (numeric or string, optional) The amount in QTUM to send. eg 0.1, default: 0
4. gasLimit  (numeric or string, optional) gasLimit, default: 250000, max: 40000000
5. gasPrice  (numeric or string, optional) gasPrice Qtum price per gas unit, default: 0.0000004, min:0.0000004
6. "senderaddress" (string, optional) The quantum address that will be used as sender.
7. "broadcast" (bool, optional, default=true) Whether to broadcast the transaction or not.
8. "changeToSender" (bool, optional, default=true) Return the change to the sender.

Result:
[
  {
    "txid" : (string) The transaction id.
    "sender" : (string) QTUM address of the sender.
    "hash160" : (string) ripemd-160 hash of the sender.
  }
]

Examples:
> qtum-cli sendtocontract "c6ca2697719d00446d4ea51f6fac8fd1e9310214" "54f6127f"
> qtum-cli sendtocontract "c6ca2697719d00446d4ea51f6fac8fd1e9310214" "54f6127f" 12.0015 6000000 0.0000004 "QM72Sfpbz1BPpXFHz9m3CdqATR44Jvaydd"

以太的创建合约与调用

创建合约

  • https://github.com/ethereum/wiki/wiki/JavaScript-API#web3ethsendtransaction
// compiled solidity source code using https://chriseth.github.io/cpp-ethereum/
var code = "603d80600c6000396000f3007c01000000000000000000000000000000000000000000000000000000006000350463c6888fa18114602d57005b6007600435028060005260206000f3";

web3.eth.sendTransaction({data: code}, function(err, transactionHash) {
  if (!err)
    console.log(transactionHash); // "0x7f9fade1c0d57a7af66ab4ead7c2eb7b11a91385"
});
  • from: String - The address for the sending account. Uses the web3.eth.defaultAccount property, if not specified.
  • to: String - (optional) The destination address of the message, left undefined for a contract-creation transaction.
  • value: Number|String|BigNumber - (optional) The value transferred for the transaction in Wei, also the endowment if it's a contract-creation transaction.
  • gas: Number|String|BigNumber - (optional, default: To-Be-Determined) The amount of gas to use for the transaction (unused gas is refunded).
  • gasPrice: Number|String|BigNumber - (optional, default: To-Be-Determined) The price of gas for this transaction in wei, defaults to the mean network gas price.
  • data: String - (optional) Either a byte string containing the associated data of the message, or in the case of a contract-creation transaction, the initialisation code.
  • nonce: Number - (optional) Integer of a nonce. This allows to overwrite your own pending transactions that use the same nonce.

https://github.com/ethereum/go-ethereum/blob/v1.7.3/internal/ethapi/api.go#L339,L371

// SendTransaction will create a transaction from the given arguments and
// tries to sign it with the key associated with args.To. If the given passwd isn't
// able to decrypt the key it fails.
func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs, passwd string) (common.Hash, error) {
    // Look up the wallet containing the requested signer
    account := accounts.Account{Address: args.From}

    wallet, err := s.am.Find(account)
    if err != nil {
        return common.Hash{}, err
    }

    if args.Nonce == nil {
        // Hold the addresse's mutex around signing to prevent concurrent assignment of
        // the same nonce to multiple accounts.
        s.nonceLock.LockAddr(args.From)
        defer s.nonceLock.UnlockAddr(args.From)
    }

    // Set some sanity defaults and terminate on failure
    if err := args.setDefaults(ctx, s.b); err != nil {
        return common.Hash{}, err
    }
    // Assemble the transaction and sign with the wallet
    tx := args.toTransaction()

    var chainID *big.Int
    if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) {
        chainID = config.ChainId
    }
    signed, err := wallet.SignTxWithPassphrase(account, passwd, tx, chainID)
    if err != nil {
        return common.Hash{}, err
    }
    return submitTransaction(ctx, s.b, signed)
}
...
// submitTransaction is a helper function that submits tx to txPool and logs a message.
func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
    if err := b.SendTx(ctx, tx); err != nil {
        return common.Hash{}, err
    }
    if tx.To() == nil { //如果目标地址为空, 则记录该TX为contract创建
        signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number())
        from, err := types.Sender(signer, tx)
        if err != nil {
            return common.Hash{}, err
        }
        addr := crypto.CreateAddress(from, tx.Nonce()) 
        log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex())
    } else {
        log.Info("Submitted transaction", "fullhash", tx.Hash().Hex(), "recipient", tx.To())
    }
    return tx.Hash(), nil
}

合约地址的生成

// Creates an ethereum address given the bytes and the nonce
func CreateAddress(b common.Address, nonce uint64) common.Address {
    data, _ := rlp.EncodeToBytes([]interface{}{b, nonce})
    return common.BytesToAddress(Keccak256(data)[12:])
}
def mk_contract_address(sender, nonce):
    return sha3(rlp.encode([normalize_address(sender), nonce]))[12:]

读取数据(非修改状态)

Call is to be used only when the request does not modify the state of the blockchain (typically only reading fields or calling constant marked functions.

  • Creating contract instance: asynchronous call (using sendTransaction). Completion is managed through the callback, called once with the Tx hash and a second time with the contract address.
  • Reading contract data: synchronous call to the local node via the call methodology.
  • Updating data: asynchronous call (using sendTransaction) with methods that do not return values. Call returns synchronously (or eventually async.) the Tx hash that can then be monitored for completion via the getTransactionReceipt (using the eth.filter('latest') that tells when the next block has been mined)
  • Receiving info asynchronously: Use of event in the contract. Monitor the reception via eth.filter( options, callback ).
  • Catching up with events after a disconnected period: use eth.filter({fromBlock: xyz, toBlock: eth.currentBlock, ...}).get()