diff --git a/qa/rpc-tests/cryptoconditions.py b/qa/rpc-tests/cryptoconditions.py index 2e5dad26e..e7d3065cc 100755 --- a/qa/rpc-tests/cryptoconditions.py +++ b/qa/rpc-tests/cryptoconditions.py @@ -149,12 +149,137 @@ class CryptoConditionsTest (BitcoinTestFramework): result = rpc.dicelist() assert_equal(result, []) + # creating dice plan with too long name (>8 chars) + result = rpc.dicefund("THISISTOOLONG", "10000", "10", "10000", "10", "5") + assert_error(result) + + # creating dice plan with < 100 funding + result = rpc.dicefund("LUCKY","10","1","10000","10","5") + assert_error(result) + + # creating dice plan with 0 blocks timeout + result = rpc.dicefund("LUCKY","10","1","10000","10","0") + assert_error(result) + + # creating dice plan + dicefundtx = rpc.dicefund("LUCKY","1000","1","800","10","5") + diceid = self.send_and_mine(dicefundtx['hex']) + + # checking if it in plans list now + result = rpc.dicelist() + assert_equal(result[0], diceid) + + # set dice name for futher usage + dicename = "LUCKY" + + # adding zero funds to plan + result = rpc.diceaddfunds(dicename,diceid,"0") + assert_error(result) + + # adding negative funds to plan + result = rpc.diceaddfunds(dicename,diceid,"-1") + assert_error(result) + + # adding funds to plan + addfundstx = rpc.diceaddfunds(dicename,diceid,"1100") + result = self.send_and_mine(addfundstx['hex']) + + # checking if funds added to plan + result = rpc.diceinfo(diceid) + assert_equal(result["funding"], "2100.00000000") + + # not valid dice info checking result = rpc.diceinfo("invalid") assert_error(result) - result = rpc.dicefund("THISISTOOLONG", "10000", "10", "10000", "10", "5") + # placing 0 amount bet + result = rpc.dicebet(dicename,diceid,"0","1") assert_error(result) + # placing negative amount bet + result = rpc.dicebet(dicename,diceid,"-1","1") + assert_error(result) + + # placing bet more than maxbet + result = rpc.dicebet(dicename,diceid,"900","1") + assert_error(result) + + # placing bet with amount more than funding + result = rpc.dicebet(dicename,diceid,"3000","1") + assert_error(result) + + # placing bet with potential won more than funding + result = rpc.dicebet(dicename,diceid,"750","9") + assert_error(result) + + # placing 0 odds bet + result = rpc.dicebet(dicename,diceid,"1","0") + assert_error(result) + + # placing negative odds bet + result = rpc.dicebet(dicename,diceid,"1","-1") + assert_error(result) + + # placing bet with odds more than allowed + result = rpc.dicebet(dicename,diceid,"1","11") + assert_error(result) + + # placing bet with not correct dice name + result = rpc.dicebet("nope",diceid,"100","1") + assert_error(result) + + # placing bet with not correct dice id + result = rpc.dicebet(dicename,self.pubkey,"100","1") + assert_error(result) + + # valid bet placing + placebet = rpc.dicebet(dicename,diceid,"100","1") + betid = self.send_and_mine(placebet["hex"]) + assert result, "bet placed" + + # check bet status + result = rpc.dicestatus(dicename,diceid,betid) + assert_success(result) + + # have to make some entropy for the next test + entropytx = 0 + fundingsum = 1 + while entropytx < 10: + fundingsuminput = str(fundingsum) + fundinghex = rpc.diceaddfunds(dicename,diceid,fundingsuminput) + result = self.send_and_mine(fundinghex['hex']) + entropytx = entropytx + 1 + fundingsum = fundingsum + 1 + + rpc.generate(2) + + # note initial dice funding state at this point. + # TODO: track player balance somehow (hard to do because of mining and fees) + diceinfo = rpc.diceinfo(diceid) + funding = float(diceinfo['funding']) + + # placing same amount bets with amount 1 and odds 1:2, checking if balance changed correct + losscounter = 0 + wincounter = 0 + betcounter = 0 + + while (betcounter < 10): + placebet = rpc.dicebet(dicename,diceid,"1","1") + betid = self.send_and_mine(placebet["hex"]) + finish = rpc.dicefinish(dicename,diceid,betid) + self.send_and_mine(finish["hex"]) + betresult = rpc.dicestatus(dicename,diceid,betid) + betcounter = betcounter + 1 + if betresult["status"] == "loss": + losscounter = losscounter + 1 + elif betresult["status"] == "win": + wincounter = wincounter + 1 + + # funding balance should increase if player loss, decrease if player won + fundbalanceguess = funding + losscounter - wincounter + fundinfoactual = rpc.diceinfo(diceid) + assert_equal(round(fundbalanceguess),round(float(fundinfoactual['funding']))) + def run_token_tests(self): rpc = self.nodes[0] result = rpc.tokenaddress() @@ -170,12 +295,15 @@ class CryptoConditionsTest (BitcoinTestFramework): result = rpc.tokenlist() assert_equal(result, []) + # trying to create token with negaive supply result = rpc.tokencreate("NUKE", "-1987420", "no bueno supply") assert_error(result) + # creating token with name more than 32 chars result = rpc.tokencreate("NUKE123456789012345678901234567890", "1987420", "name too long") assert_error(result) + # creating valid token result = rpc.tokencreate("DUKE", "1987.420", "Duke's custom token") assert_success(result) @@ -276,23 +404,23 @@ class CryptoConditionsTest (BitcoinTestFramework): # invalid numtokens bid result = rpc.tokenbid("-1", tokenid, "1") - assert_error(result); + assert_error(result) # invalid numtokens bid result = rpc.tokenbid("0", tokenid, "1") - assert_error(result); + assert_error(result) # invalid price bid result = rpc.tokenbid("1", tokenid, "-1") - assert_error(result); + assert_error(result) # invalid price bid result = rpc.tokenbid("1", tokenid, "0") - assert_error(result); + assert_error(result) # invalid tokenid bid result = rpc.tokenbid("100", "deadbeef", "1") - assert_error(result); + assert_error(result) tokenbid = rpc.tokenbid("100", tokenid, "10") tokenbidhex = tokenbid['hex'] @@ -330,11 +458,11 @@ class CryptoConditionsTest (BitcoinTestFramework): # invalid token transfer amount (have to add status to CC code!) randompubkey = "021a559101e355c907d9c553671044d619769a6e71d624f68bfec7d0afa6bd6a96" result = rpc.tokentransfer(tokenid,randompubkey,"0") - assert_error(result); + assert_error(result) # invalid token transfer amount (have to add status to CC code!) result = rpc.tokentransfer(tokenid,randompubkey,"-1") - assert_error(result); + assert_error(result) # valid token transfer sendtokens = rpc.tokentransfer(tokenid,randompubkey,"1") @@ -361,14 +489,31 @@ class CryptoConditionsTest (BitcoinTestFramework): result = rpc.rewardsinfo("none") assert_error(result) + # creating rewards plan with name > 8 chars, should return error + result = rpc.rewardscreatefunding("STUFFSTUFF", "7777", "25", "0", "10", "10") + assert_error(result) + + # creating rewards plan with 0 funding + result = rpc.rewardscreatefunding("STUFF", "0", "25", "0", "10", "10") + assert_error(result) + + # creating rewards plan with 0 maxdays + result = rpc.rewardscreatefunding("STUFF", "7777", "25", "0", "10", "0") + assert_error(result) + + # creating rewards plan with > 25% APR + result = rpc.rewardscreatefunding("STUFF", "7777", "30", "0", "10", "10") + assert_error(result) + + # creating valid rewards plan result = rpc.rewardscreatefunding("STUFF", "7777", "25", "0", "10", "10") assert result['hex'], 'got raw xtn' - txid = rpc.sendrawtransaction(result['hex']) - assert txid, 'got txid' + fundingtxid = rpc.sendrawtransaction(result['hex']) + assert fundingtxid, 'got txid' # confirm the above xtn rpc.generate(1) - result = rpc.rewardsinfo(txid) + result = rpc.rewardsinfo(fundingtxid) assert_success(result) assert_equal(result['name'], 'STUFF') assert_equal(result['APR'], "25.00000000") @@ -376,39 +521,38 @@ class CryptoConditionsTest (BitcoinTestFramework): assert_equal(result['maxseconds'], 864000) assert_equal(result['funding'], "7777.00000000") assert_equal(result['mindeposit'], "10.00000000") - assert_equal(result['fundingtxid'], txid) + assert_equal(result['fundingtxid'], fundingtxid) - # funding amount must be positive - result = rpc.rewardsaddfunding("STUFF", txid, "0") + # checking if new plan in rewardslist + result = rpc.rewardslist() + assert_equal(result[0], fundingtxid) + + # creating reward plan with already existing name, should return error + result = rpc.rewardscreatefunding("STUFF", "7777", "25", "0", "10", "10") assert_error(result) - result = rpc.rewardsaddfunding("STUFF", txid, "555") - assert_success(result) - fundingtxid = result['hex'] - assert fundingtxid, "got funding txid" - - result = rpc.rewardslock("STUFF", fundingtxid, "7") + # add funding amount must be positive + result = rpc.rewardsaddfunding("STUFF", fundingtxid, "-1") assert_error(result) - # the previous xtn has not been broadcasted yet - result = rpc.rewardsunlock("STUFF", fundingtxid) + # add funding amount must be positive + result = rpc.rewardsaddfunding("STUFF", fundingtxid, "0") assert_error(result) - # wrong plan name - result = rpc.rewardsunlock("SHTUFF", fundingtxid) - assert_error(result) + # adding valid funding + result = rpc.rewardsaddfunding("STUFF", fundingtxid, "555") + addfundingtxid = self.send_and_mine(result['hex']) + assert addfundingtxid, 'got funding txid' - txid = rpc.sendrawtransaction(fundingtxid) - assert txid, 'got txid from sendrawtransaction' + # checking if funding added to rewardsplan + result = rpc.rewardsinfo(fundingtxid) + assert_equal(result['funding'], "8332.00000000") - # confirm the xtn above - rpc.generate(1) - - # amount must be positive + # trying to lock funds, locking funds amount must be positive result = rpc.rewardslock("STUFF", fundingtxid, "-5") assert_error(result) - # amount must be positive + # trying to lock funds, locking funds amount must be positive result = rpc.rewardslock("STUFF", fundingtxid, "0") assert_error(result) @@ -416,25 +560,26 @@ class CryptoConditionsTest (BitcoinTestFramework): result = rpc.rewardslock("STUFF", fundingtxid, "7") assert_error(result) - # not working - #result = rpc.rewardslock("STUFF", fundingtxid, "10") - #assert_success(result) - #locktxid = result['hex'] - #assert locktxid, "got lock txid" + # locking funds in rewards plan + result = rpc.rewardslock("STUFF", fundingtxid, "10") + assert_success(result) + locktxid = result['hex'] + assert locktxid, "got lock txid" # locktxid has not been broadcast yet - #result = rpc.rewardsunlock("STUFF", locktxid) - #assert_error(result) + result = rpc.rewardsunlock("STUFF", fundingtxid, locktxid) + assert_error(result) # broadcast xtn - #txid = rpc.sendrawtransaction(locktxid) - #assert txid, 'got txid from sendrawtransaction' + txid = rpc.sendrawtransaction(locktxid) + assert txid, 'got txid from sendrawtransaction' # confirm the xtn above - #rpc.generate(1) + rpc.generate(1) - #result = rpc.rewardsunlock("STUFF", locktxid) - #assert_error(result) + # will not unlock since reward amount is less than tx fee + result = rpc.rewardsunlock("STUFF", fundingtxid, locktxid) + assert_error(result) def run_test (self): diff --git a/src/Makefile.am b/src/Makefile.am index 8b10c06e0..5252b1fa0 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -267,6 +267,14 @@ libbitcoin_server_a_SOURCES = \ cc/dice.cpp \ cc/lotto.cpp \ cc/fsm.cpp \ + cc/MofN.cpp \ + cc/oracles.cpp \ + cc/prices.cpp \ + cc/pegs.cpp \ + cc/triggers.cpp \ + cc/payments.cpp \ + cc/gateways.cpp \ + cc/channels.cpp \ cc/auction.cpp \ cc/betprotocol.cpp \ chain.cpp \ diff --git a/src/cc/CC made easy b/src/cc/CC made easy new file mode 100644 index 000000000..4158e061e --- /dev/null +++ b/src/cc/CC made easy @@ -0,0 +1,589 @@ +/****************************************************************************** + * Copyright © 2014-2018 The SuperNET Developers. * + * * + * See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at * + * the top-level directory of this distribution for the individual copyright * + * holder information and the developer policies on copyright and licensing. * + * * + * Unless otherwise agreed in a custom licensing agreement, no part of the * + * SuperNET software, including this file may be copied, modified, propagated * + * or distributed except according to the terms contained in the LICENSE file * + * * + * Removal or modification of this copyright notice is prohibited. * + * * + ******************************************************************************/ + +How to write utxo based CryptoConditions contracts for KMD chains +by jl777 + +This is not the only smart contracts methodology that is possible to build on top of OP_CHECKCRYPTOCONDITION, just the first one. All the credit for getting OP_CHECKCRYPTOCONDITION working in the Komodo codebase goes to @libscott. I am just hooking into the code that he made and tried to make it just a little easier to make new contracts. + +There is probably some fancy marketing name to use, but for now, I will just call it "CC contract" for short, knowing that it is not 100% technically accurate as the CryptoConditions aspect is not really the main attribute. However, the KMD contracts were built to make the CryptoConditions codebase that was integrated into it to be more accessible. + +Since CC contracts run native C/C++ code, it is turing complete and that means that any contract that is possible to do on any other platform will be possible to create via CC contract. + +utxo based contracts are a bit harder to start writing than for balance based contracts. However, they are much more secure as they leverage the existing bitcoin utxo system. That makes it much harder to have bugs that issue a zillion new coins from a bug, since all the CC contract operations needs to also obey the existing bitcoin utxo protocol. + +This document will be heavily example based so it will utilize many of the existing reference CC contracts. After understanding this document, you should be in a good position to start creating either a new CC contract to be integrated into komodod or to make rpc based dapps directly. + +Chapter 0 - Bitcoin Protocol Basics +There are many aspects of the bitcoin protocol that isnt needed to understand the CC contracts dependence on it. Such details will not be discussed. The primary aspect is the utxo, unspent transaction output. Just a fancy name for txid/vout, so when you sendtoaddress some coins, it creates a txid and the first output is vout.0, combine it and txid/0 is a specific utxo. + +Of course, to understand even this level of detail requires that you understand what a txid is, but there are plenty of reference materials on that. It is basically the 64 char long set of letters and numbers that you get when you send funds. + +Implicit with the utxo is that it prevents double spends. Once you spend a utxo, you cant spend it again. This is quite an important characteristic and while advanced readers will point out chain reorgs can allow a double spend, we will not confuse the issue with such details. The important thing is that given a blockchain at a specific height's blockhash, you can know if a txid/vout has been spent or not. + +There are also the transactions that are in memory waiting to be mined, the mempool. And it is possible for the utxo to be spent by a tx in the mempool. However since it isnt confirmed yet, it is still unspent at the current height, even if we are pretty sure it will be spent in the next block. + +A useful example is to think about a queue of people lined up to get into an event. They need to have a valid ticket and also to get into the queue. After some time passes, they get their ticket stamped and allowed into the event. + +In the utxo case, the ticket is the spending transaction and the event is the confirmed blockchain. The queue is the mempool. + + +Chapter 1 - OP_CHECKCRYPTOCONDITION +In the prior chapter the utxo was explained. However, the specific mechanism used to send a payment was not explained. Contrary to what most people might think, on the blockchain there are not entries that say "pay X amount to address". Instead what exists is a bitcoin script that must be satisfied in order for the funds to be able to be spent. + +Originally, there was the pay to pubkey script: + + +About as simple of a payment script that you can get. Basically the pubkey's signature is checked and if it is valid, you get to spend it. One problem satoshi realized was that with Quantum Computers such payment scripts are vulnerable! So, he made a way to have a cold address, ie. an address whose pubkey isnt known. At least it isnt known until it is spent, so it is only Quantum resistant prior to the first spend. This line of reasoning is why we have one time use addresses and a new change address for each transaction. Maybe in some ways, this is too forward thinking as it makes things a lot more confusing to use and easier to lose track of all the required private keys. + +However, it is here to stay and its script is: + + +With this, the blockchain has what maps to "pay to address", just that the address is actually a base58 encoded (prefix + pubkeyhash). Hey, if it wasnt complicated, it would be easy! + +In order to spend a p2pkh (pay to pubkey hash) utxo, you need to divulge the pubkey in addition to having a valid signature. After the first spend from an address, its security is degraded to p2pk (pay to pubkey) as its pubkey is now known. The net result is that each reused address takes 25 extra bytes on the blockchain, and that is why for addresses that are expected to be reused, I just use the p2pk script. + +Originally, bitcoin allowed any type of script opcodes to be used directly. The problem was some of them caused problems and satoshi decided to disable them and only allow standard forms of payments. Thus the p2pk and p2pkh became 99%+ of bitcoin transactions. However, going from having a fully scriptable language that can create countless payment scripts (and bugs!), to having just 2... well it was a "short term" limitation. It did last for some years but eventually a compromise p2sh script was allowed to be standard. This is a pay to script hash, so it can have a standard format as the normal p2pkh, but have infinitely more flexibility. + +