Skip to content

Commit

Permalink
Merge pull request #46 from coderofstuff/support-p2sh
Browse files Browse the repository at this point in the history
Support P2SH send address
  • Loading branch information
coderofstuff authored Jun 19, 2023
2 parents 6475f5f + 18d2b1f commit 58aa502
Show file tree
Hide file tree
Showing 42 changed files with 228 additions and 28 deletions.
2 changes: 1 addition & 1 deletion doc/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Transactions signed with ECDSA are currently not supported.
| P1 Value | Usage | CData |
| --- | --- | --- |
| 0x00 | Sending transaction metadata | `version (2)` \|\| `output_len (1)` \|\| `input_len (1)` |
| 0x01 | Sending a tx output | `value (8)` \|\| `script_public_key (32/33)` |
| 0x01 | Sending a tx output | `value (8)` \|\| `script_public_key (34/35)` |
| 0x02 | Sending a tx input | `value (8)` \|\| `tx_id (32)` \|\| `address_type (1)` \|\| `address_index (4)` \|\| `outpoint_index (1)` |
| 0x03 | Requesting for next signature | - |

Expand Down
4 changes: 2 additions & 2 deletions doc/TRANSACTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The base unit in Kaspa is the KAS and the smallest unit used in raw transaction

## Address format

Kaspa addresses begin with `kaspa:` followed by 61 base32 characters for a total of `67` bytes for Schnorr-signed addresses.
Kaspa addresses begin with `kaspa:` followed by 61 base32 characters for a total of `67` bytes for Schnorr-signed and P2SH addresses. P2SH addresses are supported only as a send address by this app.

For ECDSA-signed addresses (supported by this app only as a send address), it begins with `kaspa:` followed by 63 bytes for a total of `69` bytes.

Expand Down Expand Up @@ -54,7 +54,7 @@ Total bytes: 43 (max)
| Field | Size (bytes) | Description |
| --- | --- | --- |
| `value` | 8 | The amount of KAS in sompi that will go send to the address |
| `script_public_key` | 35 | Schnorr: `20` + public_key (32 bytes) + `ac` <br/> ECDSA: `20` + public_key (33 bytes) + `ab` |
| `script_public_key` | 35 | Schnorr: `20` + public_key (32 bytes) + `ac` <br/> ECDSA: `20` + public_key (33 bytes) + `ab` <br/> P2SH: `aa20` + public_key (32 bytes) + `87` |

### Transaction Requirements
- Fee = (total inputs amount) - (total outputs amount)
Expand Down
13 changes: 11 additions & 2 deletions src/address.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@ size_t compress_public_key(const uint8_t public_key[static 64],
address_type_e address_type,
uint8_t *out) {
size_t compressed_pub_size = 0;
if (address_type == SCHNORR) {
if (address_type == SCHNORR || address_type == P2SH) {
compressed_pub_size = 32;
memmove(out, public_key, 32);
} else {
} else if (address_type == ECDSA) {
compressed_pub_size = 33;
// If Y coord is even, first byte is 0x02. if odd then 0x03
out[0] = public_key[63] % 2 == 0 ? 0x02 : 0x03;
// We copy starting from the 2nd byte
memmove(out + 1, public_key, 32);
} else {
return 0;
}

return compressed_pub_size;
Expand All @@ -61,6 +63,9 @@ bool address_from_pubkey(const uint8_t public_key[static 64],
if (address_type == ECDSA) {
address_len = ECDSA_ADDRESS_LEN;
version = 1;
} else if (address_type == P2SH) {
address_len = SCHNORR_ADDRESS_LEN;
version = 8;
}

if (out_len < address_len) {
Expand All @@ -79,6 +84,10 @@ bool address_from_pubkey(const uint8_t public_key[static 64],
size_t compressed_pub_size =
compress_public_key(public_key, address_type, compressed_public_key);

if (compressed_pub_size == 0) {
return false;
}

// First part of the address is "kaspa:"
memmove(address, hrp, sizeof(hrp));
address[5] = ':';
Expand Down
21 changes: 15 additions & 6 deletions src/sighash.c
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,21 @@ static void calc_outputs_hash(transaction_t* tx, uint8_t* out_hash) {
inner_buffer,
2); // Write the output script version, assume 0

// First byte is always the length of the following public key
// Last byte is always 0xac (op code for normal transactions)
uint8_t script_len = tx->tx_outputs[i].script_public_key[0] + 2;
write_u64_le(inner_buffer,
0,
script_len); // Write the number of bytes of the script public key
uint8_t script_len = 0;
if (tx->tx_outputs[i].script_public_key[0] == 0xaa) {
// P2SH script public key is always 35 bytes,
// always begins with 0xaa and ends with 0x87
script_len = 35;
write_u64_le(inner_buffer, 0, 35);
} else {
// First byte is always the length of the following public key
// Last byte is always 0xac (op code for normal transactions)
script_len = tx->tx_outputs[i].script_public_key[0] + 2;
write_u64_le(inner_buffer,
0,
script_len); // Write the number of bytes of the script public key
}

hash_update(&inner_hash_writer, inner_buffer, 8);
hash_update(&inner_hash_writer, tx->tx_outputs[i].script_public_key, script_len);
}
Expand Down
34 changes: 28 additions & 6 deletions src/transaction/deserialize.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,30 @@ parser_status_e transaction_output_deserialize(buffer_t *buf, transaction_output
}

size_t script_len = (size_t) * (buf->ptr + buf->offset);
// Can only be length 32 or 33. Fail it otherwise:
if (script_len == 0x20 || script_len == 0x21) {

if (script_len == OP_BLAKE2B) {
// P2SH = 0xaa + 0x20 + (pubkey) + 0x87
// script len is actually the second byte if the first one is 0xaa
script_len = (size_t) * (buf->ptr + buf->offset + 1);

if (!buffer_can_read(buf, script_len + 3)) {
return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR;
}

uint8_t sig_op_code = *(buf->ptr + buf->offset + script_len + 2);

if (script_len == 0x20 && sig_op_code != OP_EQUAL) {
return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR;
}

memcpy(txout->script_public_key, buf->ptr + buf->offset, script_len + 3);

if (!buffer_seek_cur(buf, script_len + 3)) {
return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR;
}
} else if (script_len == 0x20 || script_len == 0x21) {
// P2PK
// Can only be length 32 or 33. Fail it otherwise:
if (!buffer_can_read(buf, script_len + 2)) {
return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR;
}
Expand All @@ -48,11 +70,11 @@ parser_status_e transaction_output_deserialize(buffer_t *buf, transaction_output
}

memcpy(txout->script_public_key, buf->ptr + buf->offset, script_len + 2);
} else {
return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR;
}

if (!buffer_seek_cur(buf, script_len + 2)) {
if (!buffer_seek_cur(buf, script_len + 2)) {
return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR;
}
} else {
return OUTPUT_SCRIPT_PUBKEY_PARSING_ERROR;
}

Expand Down
14 changes: 11 additions & 3 deletions src/transaction/serialize.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,19 @@ int transaction_output_serialize(const transaction_output_t *txout, uint8_t *out
write_u64_be(out, offset, txout->value);
offset += 8;

size_t script_len = txout->script_public_key[0];
if (txout->script_public_key[0] == OP_BLAKE2B) {
size_t script_len = txout->script_public_key[1];

memcpy(out + offset, txout->script_public_key, script_len + 2);
memcpy(out + offset, txout->script_public_key, script_len + 3);

offset += script_len + 2;
offset += script_len + 3;
} else {
size_t script_len = txout->script_public_key[0];

memcpy(out + offset, txout->script_public_key, script_len + 2);

offset += script_len + 2;
}

return (int) offset;
}
8 changes: 5 additions & 3 deletions src/transaction/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ typedef enum {
* Enumeration of op codes
*/
typedef enum {
OP_CHECKSIG = 0xac, // Used for SCHNORR output
OP_CHECKSIGECDSA = 0xab // Used for ECDSA output
OP_CHECKSIG = 0xac, // Used for SCHNORR output
OP_CHECKSIGECDSA = 0xab, // Used for ECDSA output
OP_BLAKE2B = 0xaa, // Used for P2SH (start)
OP_EQUAL = 0x87 // Used for P2SH (end)
} op_code_e;

typedef struct {
Expand All @@ -75,7 +77,7 @@ typedef struct {

typedef struct {
uint64_t value;
uint8_t script_public_key[35]; // In hex: 20 + public_key_hex + ac (34/35 bytes total)
uint8_t script_public_key[40]; // In hex: 20 + public_key_hex + ac (34/35 bytes total)
} transaction_output_t;

typedef struct {
Expand Down
8 changes: 6 additions & 2 deletions src/transaction/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ bool transaction_utils_check_encoding(const uint8_t* memo, uint64_t memo_len) {
void script_public_key_to_address(uint8_t* out_address, uint8_t* in_script_public_key) {
uint8_t public_key[64] = {0};
// public script keys begin with the length, followed by the amount of data
size_t data_len = (size_t) in_script_public_key[0];
size_t first_byte = (size_t) in_script_public_key[0];
address_type_e type = SCHNORR;
size_t address_len = SCHNORR_ADDRESS_LEN;

if (data_len == 0x21) {
if (first_byte == 0xaa) {
type = P2SH;
address_len = SCHNORR_ADDRESS_LEN;
memmove(public_key, in_script_public_key + 2, 32);
} else if (first_byte == 0x21) {
// We're dealing with ECDSA instead:
type = ECDSA;
address_len = ECDSA_ADDRESS_LEN;
Expand Down
3 changes: 2 additions & 1 deletion src/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ typedef enum {

typedef enum {
SCHNORR, // Display the 61 byte address for schnorr
ECDSA // Display the 63 byte address for ecdsa
ECDSA, // Display the 63 byte address for ecdsa
P2SH // Display the 61 byte address for p2sh (also schnorr)
} address_type_e;

/**
Expand Down
6 changes: 5 additions & 1 deletion tests/application_client/kaspa_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,11 @@ def _calc_outputs_hash(self) -> bytes:
for txout in self.tx.outputs:
inner_hash.update(txout.value.to_bytes(8, "little"))
inner_hash.update((0).to_bytes(2, "little")) # assume script version 0
inner_hash.update((txout.script_public_key[0] + 2).to_bytes(8, "little"))
if txout.script_public_key[0] == 0xaa:
inner_hash.update((35).to_bytes(8, "little"))
else:
inner_hash.update((txout.script_public_key[0] + 2).to_bytes(8, "little"))

inner_hash.update(txout.script_public_key)

return inner_hash.digest()
Expand Down
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00001.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00002.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00003.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00004.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00005.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00006.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00007.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanos/test_sign_tx_p2sh/00008.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanosp/test_sign_tx_p2sh/00000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanosp/test_sign_tx_p2sh/00001.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanosp/test_sign_tx_p2sh/00002.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanosp/test_sign_tx_p2sh/00003.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanosp/test_sign_tx_p2sh/00004.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanosp/test_sign_tx_p2sh/00005.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanosp/test_sign_tx_p2sh/00006.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanox/test_sign_tx_p2sh/00000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanox/test_sign_tx_p2sh/00001.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanox/test_sign_tx_p2sh/00002.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanox/test_sign_tx_p2sh/00003.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanox/test_sign_tx_p2sh/00004.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanox/test_sign_tx_p2sh/00005.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/nanox/test_sign_tx_p2sh/00006.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/stax/test_sign_tx_p2sh/00000.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/stax/test_sign_tx_p2sh/00001.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/snapshots/stax/test_sign_tx_p2sh/00003.png
Binary file added tests/snapshots/stax/test_sign_tx_p2sh/00004.png
56 changes: 56 additions & 0 deletions tests/test_sign_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,62 @@ def test_sign_tx_simple(firmware, backend, navigator, test_name):
assert transaction.get_sighash(0) == sighash
assert check_signature_validity(public_key, der_sig, sighash, transaction)

def test_sign_tx_p2sh(firmware, backend, navigator, test_name):
# Use the app interface instead of raw interface
client = KaspaCommandSender(backend)
# The path used for this entire test
path: str = "m/44'/111111'/0'/0/0"

# First we need to get the public key of the device in order to build the transaction
rapdu = client.get_public_key(path=path)
_, public_key, _, _ = unpack_get_public_key_response(rapdu.data)

# Create the transaction that will be sent to the device for signing
transaction = Transaction(
version=0,
inputs=[
TransactionInput(
value=1100000,
tx_id="40b022362f1a303518e2b49f86f87a317c87b514ca0f3d08ad2e7cf49d08cc70",
address_type=0,
address_index=0,
index=0,
public_key=public_key[1:33]
)
],
outputs=[
TransactionOutput(
value=1090000,
script_public_key="aa20f38031f61ca23d70844f63a477d07f0b2c2decab907c2e096e548b0e08721c7987"
)
]
)

# Send the sign device instruction.
# As it requires on-screen validation, the function is asynchronous.
# It will yield the result when the navigation is done
with client.sign_tx(transaction=transaction):
# Validate the on-screen request by performing the navigation appropriate for this device
if firmware.device.startswith("nano"):
navigator.navigate_until_text_and_compare(NavInsID.RIGHT_CLICK,
[NavInsID.BOTH_CLICK],
"Approve",
ROOT_SCREENSHOT_PATH,
test_name)
else:
navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_TAP,
[NavInsID.USE_CASE_REVIEW_CONFIRM,
NavInsID.USE_CASE_STATUS_DISMISS],
"Hold to sign",
ROOT_SCREENSHOT_PATH,
test_name)

# The device as yielded the result, parse it and ensure that the signature is correct
response = client.get_async_response().data
_, _, _, der_sig, _, sighash = unpack_sign_tx_response(response)
assert transaction.get_sighash(0) == sighash
assert check_signature_validity(public_key, der_sig, sighash, transaction)

def test_sign_tx_simple_sendint(firmware, backend, navigator, test_name):
# Use the app interface instead of raw interface
client = KaspaCommandSender(backend)
Expand Down
32 changes: 31 additions & 1 deletion unit-tests/test_address.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ static void test_schnorr_address_from_public_key(void **state) {
assert_string_equal((char *) address2, "kaspa:qrazhptjkcvrv23xz2xm8z8sfmg6jhxvmrscn7wph4k9we5tzxedwfxf0v6f8");
}

static void test_p2sh_address_from_public_key(void **state) {
uint8_t public_key[] = {
// OP_BLAKE2B, 0x20,
0xF3, 0x80, 0x31, 0xF6, 0x1C, 0xA2, 0x3D, 0x70, 0x84, 0x4F, 0x63, 0xA4, 0x77, 0xD0, 0x7F, 0x0B,
0x2C, 0x2D, 0xEC, 0xAB, 0x90, 0x7C, 0x2E, 0x09, 0x6E, 0x54, 0x8B, 0x0E, 0x08, 0x72, 0x1C, 0x79,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
// OP_EQUAL
};

uint8_t address[SCHNORR_ADDRESS_LEN + 1] = {0};

address_from_pubkey(public_key, P2SH, address, SCHNORR_ADDRESS_LEN);

assert_string_equal((char *) address, "kaspa:precqv0krj3r6uyyfa36ga7s0u9jct0v4wg8ctsfde2gkrsgwgw8jgxfzfc98");
}

static void test_ecdsa_address_from_public_key(void **state) {
// Even Y-coord test case
uint8_t public_key_even[] = {
Expand Down Expand Up @@ -90,9 +107,22 @@ static void test_ecdsa_address_from_public_key(void **state) {
assert_string_equal((char *) address_odd, "kaspa:qyp7xyqdshh6aylqct7x2je0pse4snep8glallgz8jppyaajz7y7qeq4x79fq4z");
}

static void test_invalid_type(void **state) {
// Even Y-coord test case
uint8_t public_key[1] = {0};

uint8_t address[1] = {0};

bool res = address_from_pubkey(public_key, -1, address, 1);

assert_int_equal(res, 0);
}

int main() {
const struct CMUnitTest tests[] = {cmocka_unit_test(test_schnorr_address_from_public_key),
cmocka_unit_test(test_ecdsa_address_from_public_key)};
cmocka_unit_test(test_ecdsa_address_from_public_key),
cmocka_unit_test(test_p2sh_address_from_public_key),
cmocka_unit_test(test_invalid_type)};

return cmocka_run_group_tests(tests, NULL, NULL);
}
Loading

0 comments on commit 58aa502

Please sign in to comment.