diff --git a/qa/cryptoconditions/test_integration.py b/qa/cryptoconditions/test_integration.py index 0bb004aeb..87ee59825 100644 --- a/qa/cryptoconditions/test_integration.py +++ b/qa/cryptoconditions/test_integration.py @@ -1,40 +1,100 @@ import sys import time import json +import logging +import binascii from testsupport import * -def test_basic_spend(): +@fanout_input(0) +def test_basic_spend(inp): spend0 = { - 'inputs': [ - {'txid': fanout, 'idx': 0, 'script': {'address': notary_addr}} - ], + 'inputs': [inp], "outputs": [ {"amount": 500, "script": {"condition": cond_alice}}, {"amount": 500, "script": {"address": notary_addr}} ] } - - spend0_txid = sign_and_submit(spend0) - + spend0_txid = submit(sign(spend0)) spend1 = { 'inputs': [ {'txid': spend0_txid, 'idx': 0, 'script': {"fulfillment": cond_alice}}, {'txid': spend0_txid, 'idx': 1, 'script': {'address': notary_addr}} ], - 'outputs': [ - {"amount": 1000, "script": {"address": notary_addr}} - ] + 'outputs': [{"amount": 1000, "script": {"address": notary_addr}}] } - - spend1_txid = sign_and_submit(spend1) - + spend1_txid = submit(sign(spend1)) assert rpc.getrawtransaction(spend1_txid) - print("all done!") +@fanout_input(1) +def test_fulfillment_wrong_signature(inp): + spend0 = { + 'inputs': [inp], + "outputs": [{"amount": 1000, "script": {"condition": cond_bob}}] + } + spend0_txid = submit(sign(spend0)) + spend1 = { + 'inputs': [{'txid': spend0_txid, 'idx': 0, 'script': {"fulfillment": cond_alice}}], + 'outputs': [{"amount": 1000, "script": {"address": notary_addr}}] + } + + signed = sign(spend1) + # Set the correct pubkey, signature is wrong + signed['tx']['inputs'][0]['script']['fulfillment']['publicKey'] = bob_pk + + try: + assert not submit(sign(spend1)), 'should raise an error' + except RPCError as e: + assert '16: mandatory-script-verify-flag-failed' in str(e), str(e) + + +@fanout_input(2) +def test_fulfillment_wrong_pubkey(inp): + spend0 = { + 'inputs': [inp], + "outputs": [{"amount": 1000, "script": {"condition": cond_alice}}] + } + spend0_txid = submit(sign(spend0)) + spend1 = { + 'inputs': [{'txid': spend0_txid, 'idx': 0, 'script': {"fulfillment": cond_alice}}], + 'outputs': [{"amount": 1000, "script": {"address": notary_addr}}] + } + + signed = sign(spend1) + # Set the wrong pubkey, signature is correct + signed['tx']['inputs'][0]['script']['fulfillment']['publicKey'] = bob_pk + + try: + assert not submit(signed), 'should raise an error' + except RPCError as e: + assert '16: mandatory-script-verify-flag-failed' in str(e), str(e) + + +@fanout_input(3) +def test_fulfillment_invalid_fulfillment(inp): + spend0 = { + 'inputs': [inp], + "outputs": [{"amount": 1000, "script": {"condition": cond_alice}}] + } + spend0_txid = submit(sign(spend0)) + + # Create a valid script with an invalid fulfillment payload + script = binascii.hexlify(b"\007invalid").decode('utf-8') + + spend1 = { + 'inputs': [{'txid': spend0_txid, 'idx': 0, 'script': script}], + 'outputs': [{"amount": 1000, "script": {"address": notary_addr}}] + } + + try: + assert not submit({'tx': spend1}), 'should raise an error' + except RPCError as e: + assert 'Crypto-Condition payload is invalid' in str(e), str(e) if __name__ == '__main__': - fanout = setup() - test_basic_spend() + for name, f in globals().items(): + if name.startswith('test_'): + logging.info("Running test: %s" % name) + f() diff --git a/qa/cryptoconditions/testsupport.py b/qa/cryptoconditions/testsupport.py index c33a611d2..aad64e320 100644 --- a/qa/cryptoconditions/testsupport.py +++ b/qa/cryptoconditions/testsupport.py @@ -1,8 +1,10 @@ +from __future__ import print_function import sys import json import time -import argparse +import logging import subprocess +import functools class RPCError(IOError): @@ -11,12 +13,14 @@ class RPCError(IOError): class JsonClient(object): def __getattr__(self, method): + if method[0] == '_': + return getattr(super(JsonClient, self), method) def inner(*args): return self._exec(method, args) return inner def load_response(self, data): - data = json.loads(data) + data = json.loads(data.decode("utf-8")) if data.get('error'): raise RPCError(data['error']) if 'result' in data: @@ -26,7 +30,6 @@ class JsonClient(object): def run_cmd(cmd): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) - #print >>sys.stderr, "> %s" % repr(cmd)[1:-1] assert proc.wait() == 0 return proc.stdout.read() @@ -39,9 +42,13 @@ class Hoek(JsonClient): class Komodod(JsonClient): def _exec(self, method, args): + if not hasattr(self, '_url'): + urltpl = 'http://$rpcuser:$rpcpassword@${rpchost:-127.0.0.1}:$rpcport' + cmd = ['bash', '-c', '. $KOMODO_CONF_PATH && echo -n %s' % urltpl] + self._url = run_cmd(cmd) + req = {'method': method, 'params': args, 'id': 1} - cmd = ['curl', '-s', '-H', 'Content-Type: application/json', - '-d', json.dumps(req), CONFIG['komodod_url']] + cmd = ['curl', '-s', '-H', 'Content-Type: application/json', '-d', json.dumps(req), self._url] return self.load_response(run_cmd(cmd)) @@ -50,7 +57,7 @@ hoek = Hoek() def wait_for_block(height): - print >>sys.stderr, "Waiting for block height %s" % height + logging.info("Waiting for block height %s" % height) for i in range(100): try: return rpc.getblock(str(height)) @@ -59,61 +66,55 @@ def wait_for_block(height): raise Exception('Time out waiting for block at height %s' % height) -def sign_and_submit(tx): +def sign(tx): signed = hoek.signTxBitcoin({'tx': tx, 'privateKeys': [notary_sk]}) - signed = hoek.signTxEd25519({'tx': signed['tx'], 'privateKeys': [alice_sk]}) - encoded = hoek.encodeTx(signed) + return hoek.signTxEd25519({'tx': signed['tx'], 'privateKeys': [alice_sk]}) + + +def submit(tx): + encoded = hoek.encodeTx(tx) try: rpc.getrawtransaction(encoded['txid']) - print >> sys.stderr, "Transaction already in chain: %s" % encoded['txid'] + logging.info("Transaction already in chain: %s" % encoded['txid']) return encoded['txid'] except RPCError: pass - print >> sys.stderr, "submit transaction: %s:%s" % (encoded['txid'], json.dumps(signed)) - print encoded + logging.info("submit transaction: %s:%s" % (encoded['txid'], json.dumps(tx))) return rpc.sendrawtransaction(encoded['hex']) +def get_fanout_txid(): + block = wait_for_block(1) + reward_txid = block['tx'][0] + reward_tx_raw = rpc.getrawtransaction(reward_txid) + reward_tx = hoek.decodeTx({'hex': reward_tx_raw}) + balance = reward_tx['tx']['outputs'][0]['amount'] -CONFIG = None -FANOUT_TXID = None + n_outs = 100 + remainder = balance - n_outs * 1000 + + fanout = { + 'inputs': [ + {'txid': reward_txid, 'idx': 0, 'script': {'pubkey': notary_pk}} + ], + "outputs": (100 * [ + {"amount": 1000, "script": {"address": notary_addr}} + ] + [{"amount": remainder, 'script': {'address': notary_addr}}]) + } + + return submit(sign(fanout)) -def setup(): - global CONFIG, FANOUT_TXID - if not CONFIG: - parser = argparse.ArgumentParser(description='Crypto-Condition integration suite.') - parser.add_argument('komodod_conf', help='Location of Komodod config file') - args = parser.parse_args() +def fanout_input(n): + def decorate(f): + def wrapper(): + if not hasattr(fanout_input, 'txid'): + fanout_input.txid = get_fanout_txid() + inp = {'txid': fanout_input.txid, 'idx': n, 'script': {'address': notary_addr}} + f(inp) + return functools.wraps(f)(wrapper) + return decorate - urltpl = 'http://$rpcuser:$rpcpassword@${rpchost:-127.0.0.1}:$rpcport' - cmd = ['bash', '-c', '. %s && echo -n %s' % (args.komodod_conf, urltpl)] - url = run_cmd(cmd) - - CONFIG = {'komodod_url': url} - - if not FANOUT_TXID: - block = wait_for_block(1) - reward_txid = block['tx'][0] - reward_tx_raw = rpc.getrawtransaction(reward_txid) - reward_tx = hoek.decodeTx({'hex': reward_tx_raw}) - balance = reward_tx['tx']['outputs'][0]['amount'] - - n_outs = 100 - remainder = balance - n_outs * 1000 - - fanout = { - 'inputs': [ - {'txid': reward_txid, 'idx': 0, 'script': {'pubkey': notary_pk}} - ], - "outputs": (100 * [ - {"amount": 1000, "script": {"address": notary_addr}} - ] + [{"amount": remainder, 'script': {'address': notary_addr}}]) - } - - FANOUT_TXID = sign_and_submit(fanout) - - return FANOUT_TXID notary_addr = 'RXSwmXKtDURwXP7sdqNfsJ6Ga8RaxTchxE' @@ -122,3 +123,6 @@ notary_sk = 'UxFWWxsf1d7w7K5TvAWSkeX4H95XQKwdwGv49DXwWUTzPTTjHBbU' alice_pk = '8ryTLBMnozUK4xUz7y49fjzZhxDDMK7c4mucLdbVY6jW' alice_sk = 'E4ER7uYvaSTdpQFzTXNNSTkR6jNRJyqhZPJMGuU899nY' cond_alice = {"type": "ed25519-sha-256", "publicKey": alice_pk} +bob_pk = 'C8MfEjKiFxDguacHvcM7MV83cRQ55RAHacC73xqg8qeu' +bob_sk = 'GrP1fZdUxUc1NYmu7kiNkJV4p7PKpshp1yBY7hogPUWT' +cond_bob = {"type": "ed25519-sha-256", "publicKey": bob_pk}