diff --git a/buidl/test/test_bech32.py b/buidl/test/test_bech32.py index bca726c..e49c386 100644 --- a/buidl/test/test_bech32.py +++ b/buidl/test/test_bech32.py @@ -1,9 +1,20 @@ from unittest import TestCase from buidl.bech32 import ( - encode_bech32_checksum, - decode_bech32, BECH32_ALPHABET, + bc32decode, + bc32encode, + bech32_create_checksum, + bech32_verify_checksum, + bech32m_create_checksum, + bech32m_verify_checksum, + cbor_decode, + cbor_encode, + convertbits, + decode_bech32, + encode_bech32_checksum, + group_32, + uses_only_bech32_chars, ) @@ -87,3 +98,141 @@ def test_bech32(self): self.assertEqual(got_network, network) self.assertEqual(got_version, version) self.assertEqual(got_raw, raw[2:]) + + +class Bc32Test(TestCase): + def test_bc32_roundtrip(self): + data = b"hello world" + encoded = bc32encode(data) + self.assertEqual(encoded, "dpjkcmr0ypmk7unvvsrvvse8") + decoded = bc32decode(encoded) + self.assertEqual(decoded, data) + + def test_bc32_empty(self): + encoded = bc32encode(b"") + decoded = bc32decode(encoded) + self.assertEqual(decoded, b"") + + def test_bc32_roundtrip_binary(self): + data = bytes(range(256)) + encoded = bc32encode(data) + decoded = bc32decode(encoded) + self.assertEqual(decoded, data) + + def test_bc32_invalid_mixed_case(self): + # bc32 strings should be single case + result = bc32decode("DpJkCmR0") + self.assertIsNone(result) + + def test_bc32_invalid_chars(self): + result = bc32decode("INVALIDCHARS!!!") + self.assertIsNone(result) + + def test_bc32_bad_checksum(self): + encoded = bc32encode(b"test") + # corrupt last character + corrupted = encoded[:-1] + ("q" if encoded[-1] != "q" else "p") + result = bc32decode(corrupted) + self.assertIsNone(result) + + +class CborTest(TestCase): + def test_cbor_short(self): + """Length <= 23 uses single-byte prefix.""" + data = b"\x01\x02\x03" + encoded = cbor_encode(data) + self.assertEqual(encoded, bytes.fromhex("43010203")) + self.assertEqual(cbor_decode(encoded), data) + + def test_cbor_medium(self): + """Length 24-255 uses 0x58 prefix.""" + data = bytes(range(30)) + encoded = cbor_encode(data) + self.assertEqual(encoded[0], 0x58) + self.assertEqual(encoded[1], 30) + self.assertEqual(cbor_decode(encoded), data) + + def test_cbor_large(self): + """Length 256-65535 uses 0x59 prefix.""" + data = bytes([0xAB]) * 300 + encoded = cbor_encode(data) + self.assertEqual(encoded[0], 0x59) + self.assertEqual(int.from_bytes(encoded[1:3], "big"), 300) + self.assertEqual(cbor_decode(encoded), data) + + def test_cbor_roundtrip_empty(self): + data = b"" + self.assertEqual(cbor_decode(cbor_encode(data)), data) + + def test_cbor_roundtrip_boundary_23(self): + data = bytes(range(23)) + self.assertEqual(cbor_decode(cbor_encode(data)), data) + + def test_cbor_roundtrip_boundary_24(self): + data = bytes(range(24)) + encoded = cbor_encode(data) + self.assertEqual(encoded[0], 0x58) + self.assertEqual(cbor_decode(encoded), data) + + +class ConvertbitsTest(TestCase): + def test_8_to_5(self): + result = convertbits([0, 1, 2, 3], 8, 5) + self.assertEqual(result, [0, 0, 0, 16, 4, 0, 24]) + + def test_roundtrip(self): + original = list(range(20)) + five_bit = convertbits(original, 8, 5) + back = convertbits(five_bit, 5, 8, False) + self.assertEqual(back, original) + + def test_invalid_value(self): + result = convertbits([-1], 8, 5) + self.assertIsNone(result) + + def test_value_too_large(self): + result = convertbits([256], 8, 5) + self.assertIsNone(result) + + +class Group32Test(TestCase): + def test_basic(self): + result = group_32(b"\x00\x01\x02") + self.assertEqual(result, [0, 0, 0, 16, 4]) + + +class UsesOnlyBech32CharsTest(TestCase): + def test_valid(self): + self.assertTrue(uses_only_bech32_chars("qpzry9x8gf2tvdw0s3jn54khce6mua7l")) + + def test_valid_uppercase(self): + self.assertTrue(uses_only_bech32_chars("QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L")) + + def test_invalid(self): + self.assertFalse(uses_only_bech32_chars("boi")) + + def test_empty(self): + self.assertTrue(uses_only_bech32_chars("")) + + +class Bech32ChecksumTest(TestCase): + def test_bech32_checksum_roundtrip(self): + hrp = "bc" + data = [0, 14, 20, 15, 7, 13, 26, 0] + checksum = bech32_create_checksum(hrp, data) + self.assertTrue(bech32_verify_checksum(hrp, data + checksum)) + + def test_bech32m_checksum_roundtrip(self): + hrp = "bc" + data = [1, 14, 20, 15, 7, 13, 26, 0] + checksum = bech32m_create_checksum(hrp, data) + self.assertTrue(bech32m_verify_checksum(hrp, data + checksum)) + + def test_decode_invalid_network(self): + with self.assertRaises(ValueError): + decode_bech32("xx1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq0l98cr") + + def test_encode_invalid_network(self): + raw = bytes.fromhex("00140000000000000000000000000000000000000000") + with self.assertRaises(ValueError): + encode_bech32_checksum(raw, network="invalid") diff --git a/buidl/test/test_hash.py b/buidl/test/test_hash.py index 742fa1c..b789ec0 100644 --- a/buidl/test/test_hash.py +++ b/buidl/test/test_hash.py @@ -1,9 +1,130 @@ from unittest import TestCase -from buidl.hash import hash_keyaggcoef +from buidl.hash import ( + hash_aux, + hash_challenge, + hash_keyaggcoef, + hash_keyagglist, + hash_musignonce, + hash_nonce, + hash_tapbranch, + hash_tapleaf, + hash_tapsighash, + hash_taptweak, +) class HashTest(TestCase): - def test_keyaggcoef(self): + def test_hash_keyaggcoef(self): want = "55a02026378a033a97431c5ac6a72eeec43069940a330431216895c11eff3cc7" self.assertEqual(hash_keyaggcoef(b"").hex(), want) + want2 = "611b0f73920e178c3ff2465c26050d88ec53e37e94f532e9389e22e8b22527f5" + self.assertEqual(hash_keyaggcoef(b"hello").hex(), want2) + + def test_hash_aux(self): + self.assertEqual( + hash_aux(b"").hex(), + "07fab5f97e680abb8389d1fa164281e124439468f5bd699fcbd1ae86e6405d69", + ) + self.assertEqual( + hash_aux(b"hello").hex(), + "28c62e0c486da0ba439f836f30bc05cba5c9e39ff2722c362722c8fc7aecdd64", + ) + + def test_hash_challenge(self): + self.assertEqual( + hash_challenge(b"").hex(), + "c216d352f5818b7b4beacd4ae0a26fe888080823d2a598856661bcd54f1b3713", + ) + self.assertEqual( + hash_challenge(b"hello").hex(), + "a97ff4dc59e2e158c00a7d9cf1e7d60fb090ecf5f728b6d17be7cbbb0fc572dd", + ) + + def test_hash_nonce(self): + self.assertEqual( + hash_nonce(b"").hex(), + "5301f1001a8be6253a3583927793565cef360de8bac2bdcbf37b195e699435a8", + ) + self.assertEqual( + hash_nonce(b"hello").hex(), + "11a805465ee0853d8c12cbaa6d431a0ff9e5adfa0c92d9a9a027a3f54b9e8eab", + ) + + def test_hash_tapbranch(self): + self.assertEqual( + hash_tapbranch(b"").hex(), + "53c373ec4d6f3c53c1f5fb2ff506dcefe1a0ed74874f93fa93c8214cbe9ffddf", + ) + self.assertEqual( + hash_tapbranch(b"hello").hex(), + "85078decf0533d9abbcaa0589e410b358752403b1c45cf1952f3923e06dbc88f", + ) + + def test_hash_tapleaf(self): + self.assertEqual( + hash_tapleaf(b"").hex(), + "5212c288a377d1f8164962a5a13429f9ba6a7b84e59776a52c6637df2106facb", + ) + self.assertEqual( + hash_tapleaf(b"hello").hex(), + "cc0d501f9c9c9610e44edb939ad98d59a1a98739d0e26dbe932660ae33a8196e", + ) + + def test_hash_tapsighash(self): + self.assertEqual( + hash_tapsighash(b"").hex(), + "dabc11914abcd8072900042a2681e52f8dba99ce82e224f97b5fdb7cd4b9c803", + ) + self.assertEqual( + hash_tapsighash(b"hello").hex(), + "ac12b2c18138a6ea5b05c3d05f88c7cb305fb94fe448aebdbba7b1dee05dd0eb", + ) + + def test_hash_taptweak(self): + self.assertEqual( + hash_taptweak(b"").hex(), + "8aa4229474ab0100b2d6f0687f031d1fc9d8eef92a042ad97d279bff456b15e4", + ) + self.assertEqual( + hash_taptweak(b"hello").hex(), + "38c140830ab88bf8dac2e03f0d765b6677949e5a9aab799378f49dde69c71926", + ) + + def test_hash_keyagglist(self): + self.assertEqual( + hash_keyagglist(b"").hex(), + "634f77a422b6a39257a76f2c13ae017702bacd4c49b33dad1139cdd56060d360", + ) + self.assertEqual( + hash_keyagglist(b"hello").hex(), + "c5bd0bd5c14f3d375ee03b476b8afa7209650974e564992c0ffd2b9572c6f762", + ) + + def test_hash_musignonce(self): + self.assertEqual( + hash_musignonce(b"").hex(), + "74fc59e69748b3ab4f2d378fa0b0beaa8c2a34dd65a04d7bf0b406b2f8489329", + ) + self.assertEqual( + hash_musignonce(b"hello").hex(), + "d5d765e8129eb1959a584408112484ed19f55d095545da60dfa0f19a342a1362", + ) + + def test_all_return_32_bytes(self): + """All tagged hash functions should return 32 bytes (SHA256 output).""" + fns = [ + hash_aux, + hash_challenge, + hash_keyaggcoef, + hash_keyagglist, + hash_musignonce, + hash_nonce, + hash_tapbranch, + hash_tapleaf, + hash_tapsighash, + hash_taptweak, + ] + for fn in fns: + result = fn(b"test") + self.assertEqual(len(result), 32, f"{fn.__name__} should return 32 bytes") diff --git a/buidl/test/test_mnemonic.py b/buidl/test/test_mnemonic.py index 96db653..ad052eb 100644 --- a/buidl/test/test_mnemonic.py +++ b/buidl/test/test_mnemonic.py @@ -1,6 +1,13 @@ from unittest import TestCase -from buidl.mnemonic import secure_mnemonic +from buidl.mnemonic import ( + BIP39, + InvalidBIP39Length, + InvalidChecksumWordsError, + bytes_to_mnemonic, + mnemonic_to_bytes, + secure_mnemonic, +) from buidl.hd import HDPrivateKey @@ -45,3 +52,91 @@ def test_secure_mnemonic_extra_entropy(self): secure_mnemonic(extra_entropy="not an int") with self.assertRaises(ValueError): secure_mnemonic(extra_entropy=-1) + + +class BytesToMnemonicTest(TestCase): + def test_128bit_zeros(self): + entropy = bytes(16) + mnemonic = bytes_to_mnemonic(entropy, 128) + expected = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + self.assertEqual(mnemonic, expected) + + def test_128bit_ones(self): + entropy = b"\xff" * 16 + mnemonic = bytes_to_mnemonic(entropy, 128) + expected = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong" + self.assertEqual(mnemonic, expected) + + def test_256bit_zeros(self): + entropy = bytes(32) + mnemonic = bytes_to_mnemonic(entropy, 256) + expected = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" + self.assertEqual(mnemonic, expected) + + def test_invalid_num_bits(self): + with self.assertRaises(InvalidBIP39Length): + bytes_to_mnemonic(bytes(16), 100) + + def test_roundtrip_all_sizes(self): + """Test that bytes_to_mnemonic -> mnemonic_to_bytes is identity.""" + for num_bits in (128, 160, 192, 224, 256): + num_bytes = num_bits // 8 + entropy = bytes(range(num_bytes)) + mnemonic = bytes_to_mnemonic(entropy, num_bits) + recovered = mnemonic_to_bytes(mnemonic) + self.assertEqual(recovered, entropy) + + +class MnemonicToBytesTest(TestCase): + def test_known_vector(self): + mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + result = mnemonic_to_bytes(mnemonic) + self.assertEqual(result, bytes(16)) + + def test_invalid_word_count(self): + with self.assertRaises(InvalidBIP39Length): + mnemonic_to_bytes("abandon abandon abandon") + + def test_invalid_checksum(self): + # Replace last word to create bad checksum + bad_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon" + with self.assertRaises(InvalidChecksumWordsError): + mnemonic_to_bytes(bad_mnemonic) + + def test_unknown_word(self): + bad_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon notaword" + with self.assertRaises(KeyError): + mnemonic_to_bytes(bad_mnemonic) + + +class WordListTest(TestCase): + def test_index_by_word(self): + self.assertEqual(BIP39["abandon"], 0) + self.assertEqual(BIP39["zoo"], 2047) + + def test_index_by_int(self): + self.assertEqual(BIP39[0], "abandon") + self.assertEqual(BIP39[2047], "zoo") + + def test_abbreviated_lookup(self): + """Words longer than 4 chars can be looked up by first 4 chars.""" + self.assertEqual(BIP39["aban"], BIP39["abandon"]) + self.assertEqual(BIP39["abst"], BIP39["abstract"]) + + def test_contains(self): + self.assertIn("abandon", BIP39) + self.assertNotIn("notaword", BIP39) + + def test_iter(self): + words = list(BIP39) + self.assertEqual(len(words), 2048) + self.assertEqual(words[0], "abandon") + self.assertEqual(words[-1], "zoo") + + def test_invalid_key_type(self): + with self.assertRaises(KeyError): + BIP39[3.14] + + def test_normalize(self): + self.assertEqual(BIP39.normalize("ABANDON"), "abandon") + self.assertEqual(BIP39.normalize("Zoo"), "zoo") diff --git a/buidl/test/test_op.py b/buidl/test/test_op.py index 00fff2f..6d5b898 100644 --- a/buidl/test/test_op.py +++ b/buidl/test/test_op.py @@ -3,11 +3,69 @@ from buidl.op import ( decode_num, encode_num, + encode_minimal_num, + number_to_op_code, + number_to_op_code_byte, + op_code_to_number, + op_0, + op_1, + op_1negate, + op_add, + op_sub, + op_1add, + op_1sub, + op_negate, + op_abs, + op_not, + op_0notequal, + op_booland, + op_boolor, + op_numequal, + op_numequalverify, + op_numnotequal, + op_lessthan, + op_greaterthan, + op_lessthanorequal, + op_greaterthanorequal, + op_min, + op_max, + op_within, + op_equal, + op_equalverify, + op_dup, + op_2dup, + op_3dup, + op_drop, + op_2drop, + op_swap, + op_rot, + op_over, + op_2over, + op_2rot, + op_2swap, + op_nip, + op_tuck, + op_pick, + op_roll, + op_ifdup, + op_depth, + op_size, + op_toaltstack, + op_fromaltstack, + op_verify, + op_return, + op_nop, + op_if, + op_notif, + op_ripemd160, + op_sha1, + op_sha256, + op_hash160, + op_hash256, op_checklocktimeverify, op_checkmultisig, op_checksequenceverify, op_checksig, - op_hash160, ) from buidl.script import Script from buidl.timelock import Locktime, Sequence @@ -16,12 +74,447 @@ from buidl.test import OfflineTestCase -class OpTest(OfflineTestCase): +class EncodeDecodeNumTest(OfflineTestCase): + def test_zero(self): + self.assertEqual(encode_num(0), b"") + self.assertEqual(decode_num(b""), 0) + + def test_positive_small(self): + self.assertEqual(decode_num(encode_num(1)), 1) + self.assertEqual(decode_num(encode_num(127)), 127) + + def test_positive_boundary(self): + # 128 requires extra byte because top bit is sign bit + encoded = encode_num(128) + self.assertEqual(encoded, b"\x80\x00") + self.assertEqual(decode_num(encoded), 128) + + def test_positive_large(self): + self.assertEqual(decode_num(encode_num(255)), 255) + self.assertEqual(decode_num(encode_num(256)), 256) + self.assertEqual(decode_num(encode_num(1000)), 1000) + self.assertEqual(decode_num(encode_num(65535)), 65535) + + def test_negative_small(self): + self.assertEqual(decode_num(encode_num(-1)), -1) + self.assertEqual(decode_num(encode_num(-127)), -127) + + def test_negative_boundary(self): + encoded = encode_num(-128) + self.assertEqual(encoded, b"\x80\x80") + self.assertEqual(decode_num(encoded), -128) + + def test_negative_large(self): + self.assertEqual(decode_num(encode_num(-255)), -255) + self.assertEqual(decode_num(encode_num(-1000)), -1000) + + def test_roundtrip(self): + for n in range(-1000, 1001): + self.assertEqual(decode_num(encode_num(n)), n) + + +class NumberToOpCodeTest(OfflineTestCase): + def test_number_to_op_code(self): + self.assertEqual(number_to_op_code(0), 0) + self.assertEqual(number_to_op_code(1), 81) + self.assertEqual(number_to_op_code(16), 96) + self.assertEqual(number_to_op_code(-1), 79) + + def test_number_to_op_code_invalid(self): + with self.assertRaises(ValueError): + number_to_op_code(-2) + with self.assertRaises(ValueError): + number_to_op_code(17) + + def test_number_to_op_code_byte(self): + self.assertEqual(number_to_op_code_byte(0), b"\x00") + self.assertEqual(number_to_op_code_byte(1), b"\x51") + self.assertEqual(number_to_op_code_byte(16), b"\x60") + self.assertEqual(number_to_op_code_byte(-1), b"\x4f") + + def test_op_code_to_number(self): + self.assertEqual(op_code_to_number(0), 0) + self.assertEqual(op_code_to_number(81), 1) + self.assertEqual(op_code_to_number(96), 16) + self.assertEqual(op_code_to_number(79), -1) + + def test_op_code_to_number_invalid(self): + with self.assertRaises(ValueError): + op_code_to_number(1) + with self.assertRaises(ValueError): + op_code_to_number(78) + + def test_encode_minimal_num(self): + # -1 to 16 should return op codes + for n in range(-1, 17): + self.assertEqual(encode_minimal_num(n), number_to_op_code(n)) + # outside range should return encode_num + self.assertEqual(encode_minimal_num(17), encode_num(17)) + self.assertEqual(encode_minimal_num(-2), encode_num(-2)) + + +class StackOpTest(OfflineTestCase): + def test_op_dup(self): + stack = [b"\x01"] + self.assertTrue(op_dup(stack)) + self.assertEqual(stack, [b"\x01", b"\x01"]) + + def test_op_dup_empty(self): + self.assertFalse(op_dup([])) + + def test_op_2dup(self): + stack = [b"\x01", b"\x02"] + self.assertTrue(op_2dup(stack)) + self.assertEqual(stack, [b"\x01", b"\x02", b"\x01", b"\x02"]) + + def test_op_3dup(self): + stack = [b"\x01", b"\x02", b"\x03"] + self.assertTrue(op_3dup(stack)) + self.assertEqual(len(stack), 6) + + def test_op_drop(self): + stack = [b"\x01", b"\x02"] + self.assertTrue(op_drop(stack)) + self.assertEqual(stack, [b"\x01"]) + + def test_op_2drop(self): + stack = [b"\x01", b"\x02", b"\x03"] + self.assertTrue(op_2drop(stack)) + self.assertEqual(stack, [b"\x01"]) + + def test_op_swap(self): + stack = [b"\x01", b"\x02"] + self.assertTrue(op_swap(stack)) + self.assertEqual(stack, [b"\x02", b"\x01"]) + + def test_op_rot(self): + stack = [b"\x01", b"\x02", b"\x03"] + self.assertTrue(op_rot(stack)) + self.assertEqual(stack, [b"\x02", b"\x03", b"\x01"]) + + def test_op_over(self): + stack = [b"\x01", b"\x02"] + self.assertTrue(op_over(stack)) + self.assertEqual(stack, [b"\x01", b"\x02", b"\x01"]) + + def test_op_2over(self): + stack = [b"\x01", b"\x02", b"\x03", b"\x04"] + self.assertTrue(op_2over(stack)) + self.assertEqual(stack, [b"\x01", b"\x02", b"\x03", b"\x04", b"\x01", b"\x02"]) + + def test_op_2rot(self): + stack = [b"\x01", b"\x02", b"\x03", b"\x04", b"\x05", b"\x06"] + self.assertTrue(op_2rot(stack)) + self.assertEqual(len(stack), 8) + self.assertEqual(stack[-2:], [b"\x01", b"\x02"]) + + def test_op_2swap(self): + stack = [b"\x01", b"\x02", b"\x03", b"\x04"] + self.assertTrue(op_2swap(stack)) + self.assertEqual(stack, [b"\x03", b"\x04", b"\x01", b"\x02"]) + + def test_op_nip(self): + stack = [b"\x01", b"\x02"] + self.assertTrue(op_nip(stack)) + self.assertEqual(stack, [b"\x02"]) + + def test_op_tuck(self): + stack = [b"\x01", b"\x02"] + self.assertTrue(op_tuck(stack)) + self.assertEqual(stack, [b"\x02", b"\x01", b"\x02"]) + + def test_op_pick(self): + stack = [b"\x01", b"\x02", b"\x03", encode_num(2)] + self.assertTrue(op_pick(stack)) + self.assertEqual(stack[-1], b"\x01") + + def test_op_roll(self): + stack = [b"\x01", b"\x02", b"\x03", encode_num(2)] + self.assertTrue(op_roll(stack)) + self.assertEqual(stack, [b"\x02", b"\x03", b"\x01"]) + + def test_op_ifdup_nonzero(self): + stack = [encode_num(1)] + self.assertTrue(op_ifdup(stack)) + self.assertEqual(len(stack), 2) + + def test_op_ifdup_zero(self): + stack = [encode_num(0)] + self.assertTrue(op_ifdup(stack)) + self.assertEqual(len(stack), 1) + + def test_op_depth(self): + stack = [b"\x01", b"\x02", b"\x03"] + self.assertTrue(op_depth(stack)) + self.assertEqual(decode_num(stack[-1]), 3) + + def test_op_size(self): + stack = [b"\x01\x02\x03"] + self.assertTrue(op_size(stack)) + self.assertEqual(decode_num(stack[-1]), 3) + + def test_op_toaltstack_fromaltstack(self): + stack = [b"\x01", b"\x02"] + altstack = [] + self.assertTrue(op_toaltstack(stack, altstack)) + self.assertEqual(stack, [b"\x01"]) + self.assertEqual(altstack, [b"\x02"]) + self.assertTrue(op_fromaltstack(stack, altstack)) + self.assertEqual(stack, [b"\x01", b"\x02"]) + self.assertEqual(altstack, []) + + def test_op_fromaltstack_empty(self): + self.assertFalse(op_fromaltstack([], [])) + + +class ArithmeticOpTest(OfflineTestCase): + def test_op_add(self): + stack = [encode_num(2), encode_num(3)] + self.assertTrue(op_add(stack)) + self.assertEqual(decode_num(stack[0]), 5) + + def test_op_add_negative(self): + stack = [encode_num(-5), encode_num(3)] + self.assertTrue(op_add(stack)) + self.assertEqual(decode_num(stack[0]), -2) + + def test_op_sub(self): + stack = [encode_num(5), encode_num(3)] + self.assertTrue(op_sub(stack)) + self.assertEqual(decode_num(stack[0]), 2) + + def test_op_1add(self): + stack = [encode_num(5)] + self.assertTrue(op_1add(stack)) + self.assertEqual(decode_num(stack[0]), 6) + + def test_op_1sub(self): + stack = [encode_num(5)] + self.assertTrue(op_1sub(stack)) + self.assertEqual(decode_num(stack[0]), 4) + + def test_op_negate(self): + stack = [encode_num(5)] + self.assertTrue(op_negate(stack)) + self.assertEqual(decode_num(stack[0]), -5) + stack = [encode_num(-3)] + self.assertTrue(op_negate(stack)) + self.assertEqual(decode_num(stack[0]), 3) + + def test_op_abs(self): + stack = [encode_num(-5)] + self.assertTrue(op_abs(stack)) + self.assertEqual(decode_num(stack[0]), 5) + stack = [encode_num(5)] + self.assertTrue(op_abs(stack)) + self.assertEqual(decode_num(stack[0]), 5) + + def test_op_not(self): + stack = [encode_num(0)] + self.assertTrue(op_not(stack)) + self.assertEqual(decode_num(stack[0]), 1) + stack = [encode_num(5)] + self.assertTrue(op_not(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_0notequal(self): + stack = [encode_num(0)] + self.assertTrue(op_0notequal(stack)) + self.assertEqual(decode_num(stack[0]), 0) + stack = [encode_num(5)] + self.assertTrue(op_0notequal(stack)) + self.assertEqual(decode_num(stack[0]), 1) + + def test_op_booland(self): + stack = [encode_num(1), encode_num(1)] + self.assertTrue(op_booland(stack)) + self.assertEqual(decode_num(stack[0]), 1) + stack = [encode_num(1), encode_num(0)] + self.assertTrue(op_booland(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_boolor(self): + stack = [encode_num(0), encode_num(0)] + self.assertTrue(op_boolor(stack)) + self.assertEqual(decode_num(stack[0]), 0) + stack = [encode_num(1), encode_num(0)] + self.assertTrue(op_boolor(stack)) + self.assertEqual(decode_num(stack[0]), 1) + + def test_op_numequal(self): + stack = [encode_num(5), encode_num(5)] + self.assertTrue(op_numequal(stack)) + self.assertEqual(decode_num(stack[0]), 1) + stack = [encode_num(5), encode_num(4)] + self.assertTrue(op_numequal(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_numnotequal(self): + stack = [encode_num(5), encode_num(4)] + self.assertTrue(op_numnotequal(stack)) + self.assertEqual(decode_num(stack[0]), 1) + stack = [encode_num(5), encode_num(5)] + self.assertTrue(op_numnotequal(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_lessthan(self): + stack = [encode_num(3), encode_num(5)] + self.assertTrue(op_lessthan(stack)) + self.assertEqual(decode_num(stack[0]), 1) + stack = [encode_num(5), encode_num(3)] + self.assertTrue(op_lessthan(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_greaterthan(self): + stack = [encode_num(5), encode_num(3)] + self.assertTrue(op_greaterthan(stack)) + self.assertEqual(decode_num(stack[0]), 1) + stack = [encode_num(3), encode_num(5)] + self.assertTrue(op_greaterthan(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_lessthanorequal(self): + stack = [encode_num(5), encode_num(5)] + self.assertTrue(op_lessthanorequal(stack)) + self.assertEqual(decode_num(stack[0]), 1) + + def test_op_greaterthanorequal(self): + stack = [encode_num(5), encode_num(5)] + self.assertTrue(op_greaterthanorequal(stack)) + self.assertEqual(decode_num(stack[0]), 1) + + def test_op_min(self): + stack = [encode_num(3), encode_num(5)] + self.assertTrue(op_min(stack)) + self.assertEqual(decode_num(stack[0]), 3) + + def test_op_max(self): + stack = [encode_num(3), encode_num(5)] + self.assertTrue(op_max(stack)) + self.assertEqual(decode_num(stack[0]), 5) + + def test_op_within(self): + # 3 is within [2, 5) + stack = [encode_num(3), encode_num(2), encode_num(5)] + self.assertTrue(op_within(stack)) + self.assertEqual(decode_num(stack[0]), 1) + # 5 is NOT within [2, 5) (exclusive upper bound) + stack = [encode_num(5), encode_num(2), encode_num(5)] + self.assertTrue(op_within(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_insufficient_stack(self): + self.assertFalse(op_add([encode_num(1)])) + self.assertFalse(op_sub([])) + self.assertFalse(op_1add([])) + self.assertFalse(op_negate([])) + self.assertFalse(op_lessthan([encode_num(1)])) + + +class EqualOpTest(OfflineTestCase): + def test_op_equal_true(self): + stack = [b"\x01\x02", b"\x01\x02"] + self.assertTrue(op_equal(stack)) + self.assertEqual(decode_num(stack[0]), 1) + + def test_op_equal_false(self): + stack = [b"\x01", b"\x02"] + self.assertTrue(op_equal(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_equalverify_true(self): + stack = [b"\x01", b"\x01"] + self.assertTrue(op_equalverify(stack)) + self.assertEqual(len(stack), 0) + + def test_op_equalverify_false(self): + stack = [b"\x01", b"\x02"] + self.assertFalse(op_equalverify(stack)) + + +class CryptoOpTest(OfflineTestCase): def test_op_hash160(self): stack = [b"hello world"] self.assertTrue(op_hash160(stack)) self.assertEqual(stack[0].hex(), "d7d5ee7824ff93f94c3055af9382c86c68b5ca92") + def test_op_sha256(self): + stack = [b""] + self.assertTrue(op_sha256(stack)) + self.assertEqual( + stack[0].hex(), + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) + + def test_op_sha1(self): + stack = [b""] + self.assertTrue(op_sha1(stack)) + self.assertEqual(stack[0].hex(), "da39a3ee5e6b4b0d3255bfef95601890afd80709") + + def test_op_ripemd160(self): + stack = [b""] + self.assertTrue(op_ripemd160(stack)) + self.assertEqual(stack[0].hex(), "9c1185a5c5e9fc54612808977ee8f548b2258d31") + + def test_op_hash256(self): + stack = [b""] + self.assertTrue(op_hash256(stack)) + # hash256 = sha256(sha256(x)) + self.assertEqual(len(stack[0]), 32) + + def test_crypto_ops_empty_stack(self): + self.assertFalse(op_sha256([])) + self.assertFalse(op_sha1([])) + self.assertFalse(op_ripemd160([])) + self.assertFalse(op_hash160([])) + self.assertFalse(op_hash256([])) + + +class FlowControlTest(OfflineTestCase): + def test_op_verify_true(self): + stack = [encode_num(1)] + self.assertTrue(op_verify(stack)) + + def test_op_verify_false(self): + stack = [encode_num(0)] + self.assertFalse(op_verify(stack)) + + def test_op_verify_empty(self): + self.assertFalse(op_verify([])) + + def test_op_return(self): + self.assertFalse(op_return([])) + + def test_op_nop(self): + self.assertTrue(op_nop([])) + + def test_op_numequalverify_true(self): + stack = [encode_num(5), encode_num(5)] + self.assertTrue(op_numequalverify(stack)) + + def test_op_numequalverify_false(self): + stack = [encode_num(5), encode_num(4)] + self.assertFalse(op_numequalverify(stack)) + + +class PushNumberTest(OfflineTestCase): + def test_op_0(self): + stack = [] + self.assertTrue(op_0(stack)) + self.assertEqual(decode_num(stack[0]), 0) + + def test_op_1(self): + stack = [] + self.assertTrue(op_1(stack)) + self.assertEqual(decode_num(stack[0]), 1) + + def test_op_1negate(self): + stack = [] + self.assertTrue(op_1negate(stack)) + self.assertEqual(decode_num(stack[0]), -1) + + +class OpTest(OfflineTestCase): def test_op_checksig(self): tests = ( ( diff --git a/buidl/test/test_pbkdf2.py b/buidl/test/test_pbkdf2.py new file mode 100644 index 0000000..509792d --- /dev/null +++ b/buidl/test/test_pbkdf2.py @@ -0,0 +1,58 @@ +from unittest import TestCase + +from buidl.pbkdf2 import PBKDF2 + + +class PBKDF2Test(TestCase): + def test_rfc6070_vector1(self): + """password='password', salt='salt', iterations=1, dkLen=20""" + result = PBKDF2("password", "salt", iterations=1).read(20) + self.assertEqual(result.hex(), "0c60c80f961f0e71f3a9b524af6012062fe037a6") + + def test_rfc6070_vector2(self): + """password='password', salt='salt', iterations=2, dkLen=20""" + result = PBKDF2("password", "salt", iterations=2).read(20) + self.assertEqual(result.hex(), "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957") + + def test_rfc6070_vector3(self): + """password='password', salt='salt', iterations=4096, dkLen=20""" + result = PBKDF2("password", "salt", iterations=4096).read(20) + self.assertEqual(result.hex(), "4b007901b765489abead49d926f721d065a429c1") + + def test_bytes_input(self): + """Passphrase and salt can be bytes.""" + result = PBKDF2(b"password", b"salt", iterations=1).read(20) + self.assertEqual(result.hex(), "0c60c80f961f0e71f3a9b524af6012062fe037a6") + + def test_read_different_lengths(self): + p = PBKDF2("password", "salt", iterations=1) + key32 = p.read(32) + self.assertEqual(len(key32), 32) + + def test_hexread(self): + p = PBKDF2("password", "salt", iterations=1) + hex_result = p.hexread(20) + self.assertEqual(hex_result, "0c60c80f961f0e71f3a9b524af6012062fe037a6") + + def test_close(self): + p = PBKDF2("password", "salt", iterations=1) + p.close() + self.assertTrue(p.closed) + with self.assertRaises(ValueError): + p.read(20) + + def test_invalid_passphrase_type(self): + with self.assertRaises(TypeError): + PBKDF2(12345, "salt") + + def test_invalid_salt_type(self): + with self.assertRaises(TypeError): + PBKDF2("password", 12345) + + def test_invalid_iterations_type(self): + with self.assertRaises(TypeError): + PBKDF2("password", "salt", iterations="many") + + def test_zero_iterations(self): + with self.assertRaises(ValueError): + PBKDF2("password", "salt", iterations=0) diff --git a/buidl/test/test_siphash.py b/buidl/test/test_siphash.py new file mode 100644 index 0000000..e8a1224 --- /dev/null +++ b/buidl/test/test_siphash.py @@ -0,0 +1,112 @@ +from unittest import TestCase + +from buidl.siphash import SipHash_2_4 + + +class SipHashTest(TestCase): + def test_known_vectors(self): + """Test against the official SipHash-2-4 test vectors from the spec.""" + vectors = [ + "310e0edd47db6f72", + "fd67dc93c539f874", + "5a4fa9d909806c0d", + "2d7efbd796666785", + "b7877127e09427cf", + "8da699cd64557618", + "cee3fe586e46c9cb", + "37d1018bf50002ab", + "6224939a79f5f593", + "b0e4a90bdf82009e", + "f3b9dd94c5bb5d7a", + "a7ad6b22462fb3f4", + "fbe50e86bc8f1e75", + "903d84c02756ea14", + "eef27a8e90ca23f7", + "e545be4961ca29a1", + "db9bc2577fcc2a3f", + "9447be2cf5e99a69", + "9cd38d96f0b3c14b", + "bd6179a71dc96dbb", + "98eea21af25cd6be", + "c7673b2eb0cbf2d0", + "883ea3e395675393", + "c8ce5ccd8c030ca8", + "94af49f6c650adb8", + "eab8858ade92e1bc", + "f315bb5bb835d817", + "adcf6b0763612e2f", + "a5c91da7acaa4dde", + "716595876650a2a6", + "28ef495c53a387ad", + "42c341d8fa92d832", + "ce7cf2722f512771", + "e37859f94623f3a7", + "381205bb1ab0e012", + "ae97a10fd434e015", + "b4a31508beff4d31", + "81396229f0907902", + "4d0cf49ee5d4dcca", + "5c73336a76d8bf9a", + "d0a704536ba93e0e", + "925958fcd6420cad", + "a915c29bc8067318", + "952b79f3bc0aa6d4", + "f21df2e41d4535f9", + "87577519048f53a9", + "10a56cf5dfcd9adb", + "eb75095ccd986cd0", + "51a9cb9ecba312e6", + "96afadfc2ce666c7", + "72fe52975a4364ee", + "5a1645b276d592a1", + "b274cb8ebf87870a", + "6f9bb4203de7b381", + "eaecb2a30b22a87f", + "9924a43cc1315724", + "bd838d3aafbf8db7", + "0b1a2a3265d51aea", + "135079a3231ce660", + "932b2846e4d70666", + "e1915f5cb1eca46c", + "f325965ca16d629f", + "575ff28e60381be5", + "724506eb4c328a95", + ] + key = bytes(range(16)) + plaintext = bytes(range(64)) + for i in range(64): + result = SipHash_2_4(key, plaintext[:i]).hexdigest() + self.assertEqual(result, vectors[i].encode("utf-8"), f"failed on vector {i}") + + def test_hash_values(self): + key = b"0123456789ABCDEF" + self.assertEqual(SipHash_2_4(key, b"a").hash(), 12398370950267227270) + self.assertEqual(SipHash_2_4(key, b"").hash(), 3627314469837380007) + + def test_update_incremental(self): + key = b"FEDCBA9876543210" + h1 = SipHash_2_4(key, b"a").hash() + h2 = SipHash_2_4(key).update(b"a").hash() + self.assertEqual(h1, h2) + # update with empty string should not change result + h3 = SipHash_2_4(key).update(b"a").update(b"").hash() + self.assertEqual(h1, h3) + h4 = SipHash_2_4(key).update(b"").update(b"a").hash() + self.assertEqual(h1, h4) + + def test_copy(self): + key = b"FEDCBA9876543210" + a = SipHash_2_4(key).update(b"a") + b = a.copy() + self.assertEqual(a.hash(), b.hash()) + a.update(b"a") + # copy should be independent + self.assertNotEqual(a.hash(), b.hash()) + + def test_digest(self): + key = b"0123456789ABCDEF" + h = SipHash_2_4(key, b"a") + digest = h.digest() + self.assertEqual(len(digest), 8) + hexdigest = h.hexdigest() + self.assertEqual(len(hexdigest), 16) diff --git a/buidl/test/test_witness.py b/buidl/test/test_witness.py new file mode 100644 index 0000000..58fc7dd --- /dev/null +++ b/buidl/test/test_witness.py @@ -0,0 +1,69 @@ +from io import BytesIO +from unittest import TestCase + +from buidl.witness import Witness + + +class WitnessTest(TestCase): + def test_parse_serialize_roundtrip(self): + w = Witness([b"\x01\x02\x03", b"\x04\x05"]) + serialized = w.serialize() + self.assertEqual(serialized.hex(), "0203010203020405") + w2 = Witness.parse(BytesIO(serialized)) + self.assertEqual(w2.items, w.items) + + def test_empty_witness(self): + w = Witness() + self.assertEqual(len(w), 0) + self.assertEqual(w.serialize().hex(), "00") + w2 = Witness.parse(BytesIO(w.serialize())) + self.assertEqual(len(w2), 0) + + def test_single_item(self): + w = Witness([b"\xde\xad\xbe\xef"]) + self.assertEqual(len(w), 1) + serialized = w.serialize() + w2 = Witness.parse(BytesIO(serialized)) + self.assertEqual(w2.items, [b"\xde\xad\xbe\xef"]) + + def test_getitem(self): + w = Witness([b"\x01", b"\x02", b"\x03"]) + self.assertEqual(w[0], b"\x01") + self.assertEqual(w[1], b"\x02") + self.assertEqual(w[2], b"\x03") + self.assertEqual(w[-1], b"\x03") + + def test_len(self): + self.assertEqual(len(Witness()), 0) + self.assertEqual(len(Witness([b"\x01"])), 1) + self.assertEqual(len(Witness([b"\x01", b"\x02", b"\x03"])), 3) + + def test_repr(self): + w = Witness([b"\x01\x02", b""]) + result = repr(w) + self.assertIn("0102", result) + self.assertIn("", result) + + def test_clone(self): + w = Witness([b"\x01", b"\x02"]) + c = w.clone() + self.assertEqual(c.items, w.items) + # clone should be independent + c.items.append(b"\x03") + self.assertNotEqual(len(c), len(w)) + + def test_has_annex(self): + # Annex starts with 0x50 + w_annex = Witness([b"sig", b"\x50\x01\x02"]) + self.assertTrue(w_annex.has_annex()) + # No annex + w_no_annex = Witness([b"sig", b"\x30\x01\x02"]) + self.assertFalse(w_no_annex.has_annex()) + + def test_with_empty_items(self): + """Witness with empty byte strings (used in multisig for OP_0 bug).""" + w = Witness([b"", b"\x01\x02"]) + self.assertEqual(len(w), 2) + serialized = w.serialize() + w2 = Witness.parse(BytesIO(serialized)) + self.assertEqual(w2.items, w.items)