Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0434d6f
Fix known UI bugs
Fingolfin69 May 29, 2025
7672bdf
Update snapshots
Fingolfin69 May 29, 2025
4745f85
Add UI/support for nano devices
Fingolfin69 Jun 3, 2025
4e3a729
Update snapshots
Fingolfin69 Jun 3, 2025
e7769e5
Add nanox icon
Fingolfin69 Jun 5, 2025
880fcce
Upadte nano UI flow
Fingolfin69 Jun 10, 2025
f725459
Update flex/stax UI flow
Fingolfin69 Jun 10, 2025
4fd863d
Update snapshots
Fingolfin69 Jun 10, 2025
d11fb0b
Fix enforcer warning
Fingolfin69 Jun 10, 2025
2ca60e7
Fix spellchecking
Fingolfin69 Jun 11, 2025
8197df3
Manually detect change output
Fingolfin69 Jun 20, 2025
1338418
Fix fmt
Fingolfin69 Jun 20, 2025
cdb3824
Fix fmt II
Fingolfin69 Jun 22, 2025
5816f78
Bump base app
Fingolfin69 Jun 22, 2025
1a48633
UI fixes after base app update
Fingolfin69 Jun 22, 2025
f7ec7c6
Add 0x prefix to addresses
Fingolfin69 Jun 23, 2025
c2fe783
Update snapshots
Fingolfin69 Jun 23, 2025
aec94e0
Change SIGHASH type
Fingolfin69 Jun 26, 2025
71a14d7
Add mainnet derivation path
Fingolfin69 May 26, 2025
b7ae505
Fix unknown network
Fingolfin69 Jun 26, 2025
bbb0438
Show staking amount w/o TX fees
Fingolfin69 Jun 26, 2025
fd9a7f9
Update UI flow V2
Fingolfin69 Jun 26, 2025
7755716
Update snapshots
Fingolfin69 Jun 26, 2025
f189c5b
Update app description
Fingolfin69 Jun 27, 2025
9088384
Review fixes
Fingolfin69 Jul 2, 2025
bec1da3
Check number of outputs for TX type
Fingolfin69 Jul 3, 2025
e0b741d
Fix fmt
Fingolfin69 Jul 4, 2025
8621e33
Update docs
Fingolfin69 Jul 4, 2025
1da6b23
Fixup doc
Fingolfin69 Jul 4, 2025
c72429f
Update comment
Fingolfin69 Jul 4, 2025
d3a036f
Fix app name and description resolution for mainnet builds
sponomarev Jul 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,29 +28,28 @@ APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)"
APPDEVELOPPER="Ledger"
APPCOPYRIGHT="(c) 2025 Ledger"

VARIANT_PARAM = COIN
VARIANT_VALUES = core_dao core_dao_testnet

# Application source files
# There is no additional sources for bitcoin
#APP_SOURCE_PATH += src/

# simplify for tests
ifndef COIN
COIN=core_dao_testnet
endif

# Enabling DEBUG flag will enable PRINTF and disable optimizations
DEBUG = 10
#DEBUG = 10

APP_DESCRIPTION ="This app enables to\nstake bitcoin\non CoreDAO."
APP_DESCRIPTION ="This app enables to\nlock BTC\non CoreDAO."

ifeq ($(COIN),core_dao)
APPNAME ="CoreDAO"
BITCOIN_NETWORK =mainnet
DEFINES += CORE_DAO_MAINNET

else ifeq ($(COIN),core_dao_testnet)
APPNAME ="CoreDAO Testnet"
BITCOIN_NETWORK =testnet
DEFINES += CORE_DAO_TESTNET

else ifeq ($(filter clean,$(MAKECMDGOALS)),)
$(error Unsupported COIN - use $(VARIANT_VALUES))
Expand Down
34 changes: 28 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
## CoreDAO app

This repository implements the CoreDAO protocol for clear signing transactions of a specific format that the bitcoin app does not support.
This repository implements the CoreDAO protocol for **clear-signing** Bitcoin PSBTs whose structure is not supported by the vanilla Bitcoin app.

This application allows 3 types of transaction (stake, unstake, restake):
- Stake transaction contains internal inputs and at most 3 outputs (the locking output, a OP_RETURN output and an optional change)
- Unstake transaction spend a locking UTXO and must have exactly one output (on a change address)
- Restake contain internal inputs and an input spending the locking UTXO and at most 3 outputs (same as the Stake transaction)
### Supported transaction types

This application assumes the lock output is receivable and spendable on a unique path `84h/0h/0h/0/0`
| Type | Synonyms | Purpose in CoreDAO | Key characteristics |
|------|----------|--------------------|---------------------|
| **Lock** | “Stake” | Create a time-locked UTXO that represents the staked funds | Internal inputs only; ≤ 3 outputs (locking output ✚ CoreDAO `OP_RETURN` ✚ optional change) |
| **Unlock** | “Unstake” | Spend a previously locked UTXO back to the user after the CLTV expires | Must spend exactly one locking UTXO; produces **one** unlocking output (to a change address) |
| **Restake** | “combined unlock + lock” | Atomically release a matured stake and create a new stake in the same TX | Internal inputs ✚ the spent locking UTXO; obeys *all* lock & unlock rules **except** it must **not** have a change or unlocking output |

#### 1. General
* The dedicated lock output is always derived from the unique BIP-84 path `84h/0h/0h/0/0` (or `84h/1h/0h/0/0` for testnet).
* A PSBT may contain any number of **internal inputs** (regular wallet UTXOs).
* A PSBT may contain any number of **CoreDAO inputs** (previous lock outputs).

#### 2. Lock (stake) transactions
* If the TX contains an `OP_RETURN` output, that output **must** be a valid CoreDAO metadata output **and** the TX **must** also contain a lock output.
* The PSBT may have **at most one** change output.
* If *any* input is a CoreDAO input, the outputs are limited to **change** and **lock** outputs only (i.e., no arbitrary payments).

#### 3. Unlock (unstake) transactions
* If the TX spends a CLTV (time-locked) CoreDAO UTXO, it **must** follow the CoreDAO unlock format.
* Such a TX **must have exactly one** unlocking output (the returned funds).

#### 4. Restake (combined unlock + lock) transactions
* All rules for *both* Lock and Unlock apply **except**:
* **No change output** is allowed.
* **No unlocking output** is allowed (the funds are immediately re-locked).

By enforcing these constraints, the app guarantees that every signature produced on-device corresponds to a transaction that is valid under the CoreDAO staking model and cannot be mis-used for unintended transfers.

## Compiling the app

Expand Down
2 changes: 1 addition & 1 deletion bitcoin_app_base
Binary file removed glyphs/.DS_Store
Binary file not shown.
Binary file modified glyphs/app_logo.gif
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 glyphs/app_logo_inv.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed icons/foo.png
Binary file not shown.
Binary file added icons/nanox_app_core.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed icons/nanox_app_foo.gif
Binary file not shown.
2 changes: 1 addition & 1 deletion ledger_app.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[app]
build_directory = "./"
sdk = "C"
devices = ["flex", "stax"]
devices = ["nanox", "nanos+", "flex", "stax"]

[tests]
pytest_directory = "./tests/"
6 changes: 3 additions & 3 deletions scripts/print_psbt.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ function witnessUtxoToString(witnessUtxo) {
return (`\n Amount: ${witnessUtxo.amount}\n Script: ${toHex(witnessUtxo.scriptPubKey)}`);
}

function printBip32Derivations(psbt, inout, prefix, index) {
const funcName = `get${inout.capitalize()}Bip32Derivation`;
const mapName = `${inout}Maps`;
function printBip32Derivations(psbt, in_out, prefix, index) {
const funcName = `get${in_out.capitalize()}Bip32Derivation`;
const mapName = `${in_out}Maps`;
let firstDerivation = true;

const map = psbt[mapName][index];
Expand Down
105 changes: 73 additions & 32 deletions src/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,36 @@
#include "cx.h"
#include "ledger_assert.h"

#define EXPECTED_PAYLOAD_LEN 80 // SAT+(4)+ VERSION(1) + CHAIN_ID(2) + DELEGATOR(20) + VALIDATOR(20) + FEE(1) + REDEEM(32)
#define EXPECTED_PAYLOAD_LEN \
80 // SAT+(4)+ VERSION(1) + CHAIN_ID(2) + DELEGATOR(20) + VALIDATOR(20) + FEE(1) + REDEEM(32)

#define SUPPORTED_VERSION 1

static bool get_core_compressed_pubkey(const uint32_t bip32_path[],
uint8_t bip32_path_len,
uint8_t out_pubkey[static 33]) {
uint8_t chaincode[32];

return crypto_get_compressed_pubkey_at_path(bip32_path, bip32_path_len, out_pubkey, chaincode);
}

static bool get_core_pubkey_hash160(const uint32_t bip32_path[],
uint8_t bip32_path_len,
uint8_t out_hash160[static 20]) {
uint8_t pubkey[33];
if (!get_core_compressed_pubkey(bip32_path, bip32_path_len, pubkey)) {
return false;
}
crypto_hash160(pubkey, 33, out_hash160);
return true;
}

static const char *SAT_PLUS = "SAT+";

bool parse_staking_information(
uint8_t *payload,
uint32_t payload_len,
core_dao_tx_info_t *info,
uint8_t redeem_script[static REDEEM_SCRIPT_LEN]
) {
bool parse_staking_information(uint8_t *payload,
uint32_t payload_len,
core_dao_tx_info_t *info,
uint8_t redeem_script[static REDEEM_SCRIPT_LEN]) {
if (payload_len != EXPECTED_PAYLOAD_LEN) {
PRINT("Expected payload length %d, got %d\n", EXPECTED_PAYLOAD_LEN, payload_len);
return false;
Expand All @@ -49,8 +65,7 @@ bool parse_staking_information(

// Read chain id
info->chain_id = (payload[0] << 8) | payload[1];
if (info->chain_id != CHAID_ID_MAINNET &&
info->chain_id != CHAIN_ID_TESTNET &&
if (info->chain_id != CHAID_ID_MAINNET && info->chain_id != CHAIN_ID_TESTNET &&
info->chain_id != CHAIN_ID_TESTNET2) {
PRINT("Unsupported chain id %d\n", info->chain_id);
return false;
Expand Down Expand Up @@ -79,6 +94,7 @@ bool parse_staking_information(

bool get_core_redeem_script(uint32_t locktime, uint8_t redeem_script[static REDEEM_SCRIPT_LEN]) {
int offset = 0;
const uint32_t path[] = CORE_DERIVATION_PATH;

redeem_script[offset++] = OP_PUSHBYTES_4;
write_u32_le(redeem_script, offset, locktime);
Expand All @@ -87,8 +103,8 @@ bool get_core_redeem_script(uint32_t locktime, uint8_t redeem_script[static REDE
redeem_script[offset++] = OP_DROP;
redeem_script[offset++] = OP_DUP;
redeem_script[offset++] = OP_HASH160;
redeem_script[offset++] = OP_PUSHBYTES_20; // Push 20 bytes
if (!get_core_pubkey_hash160(redeem_script + offset)) {
redeem_script[offset++] = OP_PUSHBYTES_20; // Push 20 bytes
if (!get_core_pubkey_hash160(path, CORE_DERIVATION_PATH_LEN, redeem_script + offset)) {
return false;
}
offset += 20;
Expand All @@ -110,22 +126,20 @@ bool validate_redeem_script(uint8_t redeem_script[static REDEEM_SCRIPT_LEN]) {
return memcmp(redeem_script, expected_redeem_script, REDEEM_SCRIPT_LEN) == 0;
}

bool validate_lock_script_pubkey(
uint8_t *lock_script_pubkey,
size_t lock_script_pubkey_len,
uint8_t *redeem_script
) {
bool validate_lock_script_pubkey(uint8_t *lock_script_pubkey,
size_t lock_script_pubkey_len,
uint8_t *redeem_script) {
uint8_t expected_redeem_script[REDEEM_SCRIPT_LEN];
uint8_t script_hash[32];
uint32_t locktime;
uint32_t locktime;

// Read locktime from redeem script
locktime = read_u32_le(redeem_script, 1);

if (!get_core_redeem_script(locktime, expected_redeem_script)) {
return false;
}

if (cx_hash_sha256(expected_redeem_script, REDEEM_SCRIPT_LEN, script_hash, 32) != 32) {
return false;
}
Expand All @@ -140,24 +154,51 @@ bool validate_lock_script_pubkey(
return memcmp(lock_script_pubkey + 2, script_hash, 32) == 0;
}

bool get_core_compressed_pubkey(uint8_t pubkey[static 33]) {
uint32_t path[] = CORE_DERIVATION_PATH;
uint8_t chaincode[32];

return crypto_get_compressed_pubkey_at_path(path, CORE_DERIVATION_PATH_LEN, pubkey, chaincode);
}
bool check_if_change_output(const uint32_t bip32_path[],
uint8_t bip32_path_len,
const uint8_t *script,
int32_t script_len) {
uint8_t hash[20];

bool get_core_pubkey_hash160(uint8_t hash160[static 20]) {
uint8_t pubkey[33];
if (!get_core_compressed_pubkey(pubkey)) {
if (script_len != P2WPKH_SCRIPT_LEN) {
return false;
}
crypto_hash160(pubkey, 33, hash160);
return true;

if (script[0] != OP_0 || script[1] != OP_PUSHBYTES_20) {
return false;
}

if (!get_core_pubkey_hash160(bip32_path, bip32_path_len, hash)) {
return false;
}

return memcmp(hash, script + 2, HASH160_LEN) == 0;
}

void buffer_to_hex(uint8_t *buffer, size_t buffer_len, char *out, size_t out_len) {
for (size_t i = 0; i < buffer_len && i < out_len; i++) {
snprintf(out + i * 2, out_len - i * 2, "%02x", buffer[i]);
void buffer_to_hex(const uint8_t *buffer, size_t buffer_len, char *out, size_t out_len) {
static const char lut[] = "0123456789abcdef";

// Each byte needs two chars. Reserve one byte for '\0'
size_t max_bytes = (out_len - 1) / 2;

size_t i;
for (i = 0; i < buffer_len && i < max_bytes; ++i) {
uint8_t byte = buffer[i];
out[i * 2] = lut[byte >> 4]; /* high nibble */
out[i * 2 + 1] = lut[byte & 0x0F]; /* low nibble */
}

// Always terminate the string
out[i * 2] = '\0';
}

void format_address(const uint8_t *buffer, size_t buffer_len, char *out, size_t out_len) {
// Add "0x" prefix
out[0] = '0';
out[1] = 'x';

out += 2;
out_len -= 2;

buffer_to_hex(buffer, buffer_len, out, out_len);
}
73 changes: 45 additions & 28 deletions src/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,31 @@
#define MAX_DERIVATION_PATH_DEPTH 4

#define REDEEM_SCRIPT_LEN 32
#define SCRIPT_HASH_LEN 32
#define LOCK_SCRIPT_LEN 34
#define CHAID_ID_MAINNET 1116
#define CHAIN_ID_TESTNET 1115
#define SCRIPT_HASH_LEN 32
#define LOCK_SCRIPT_LEN 34
#define CHAID_ID_MAINNET 1116
#define CHAIN_ID_TESTNET 1115
#define CHAIN_ID_TESTNET2 1114

#define P2WPKH_SCRIPT_LEN 22 // OP_0 + OP_PUSHBYTES_20 + 20 bytes of hash
#define HASH160_LEN 20

#define H 0x80000000

// Useful OP_CODES
#define OP_PUSHBYTES_4 4
#define OP_PUSHBYTES_4 4
#define OP_PUSHBYTES_20 20
#define OP_PUSHBYTES_32 32

#define CORE_DERIVATION_PATH {84 | H, 1 | H, 0 | H, 0, 0}
#ifdef CORE_DAO_MAINNET
#define CORE_DERIVATION_PATH \
{ 84 | H, 0 | H, 0 | H, 0, 0 }
#elif CORE_DAO_TESTNET
#define CORE_DERIVATION_PATH \
{ 84 | H, 1 | H, 0 | H, 0, 0 }
#else
#error "Net type must be defined. Define CORE_DAO_MAINNET or CORE_DAO_TESTNET"
#endif
#define CORE_DERIVATION_PATH_LEN 5

typedef enum {
Expand All @@ -30,51 +41,57 @@ typedef enum {
} tx_type_t;

typedef struct {
// Global informations
bool is_stacking_info_output_found;
bool is_lock_output_found;
bool is_unlock_or_change_output_found;
uint32_t unlock_or_change_output_num;
} core_dao_tx_outputs_t;

typedef struct {
// Global information
tx_type_t type;

// Stake informations
// Stake information
uint16_t chain_id;
uint8_t delegator[20];
uint8_t validator[20];
uint64_t lock_amount;
uint8_t fee;
uint32_t locktime;

// Unstake informations
// Unstake information
uint64_t unlock_amount;
uint32_t n_core_dao_inputs;
uint8_t core_inputs[64];
core_dao_tx_outputs_t found_outputs;
uint8_t core_inputs[64];
} core_dao_tx_info_t;

/***
* Parse the staking informations from an OP_RETURN output script
* @param payload The payload of the OP_RETURN output script minus the OP_RETURN opcode and the data length
* Parse the staking information from an OP_RETURN output script
* @param payload The payload of the OP_RETURN output script minus the OP_RETURN opcode and the data
length
* @param payload_len The length of the payload (should be 80 bytes)
* @param info The parsed staking information
* @param redeem_script The redeem script parsed from the payload

* @return true if the parsing was successful, false otherwise
*/
bool parse_staking_information(
uint8_t *payload,
uint32_t payload_len,
core_dao_tx_info_t *info,
uint8_t redeem_script[static REDEEM_SCRIPT_LEN]
);
bool parse_staking_information(uint8_t *payload,
uint32_t payload_len,
core_dao_tx_info_t *info,
uint8_t redeem_script[static REDEEM_SCRIPT_LEN]);

bool validate_redeem_script(uint8_t redeem_script[static REDEEM_SCRIPT_LEN]);

bool validate_lock_script_pubkey(
uint8_t *lock_script_pubkey,
size_t lock_script_pubkey_len,
uint8_t redeem_script[static REDEEM_SCRIPT_LEN]
);

bool get_core_compressed_pubkey(uint8_t pubkey[static 33]);
bool validate_lock_script_pubkey(uint8_t *lock_script_pubkey,
size_t lock_script_pubkey_len,
uint8_t redeem_script[static REDEEM_SCRIPT_LEN]);

bool get_core_pubkey_hash160(uint8_t hash160[static 20]);
bool check_if_change_output(const uint32_t bip32_path[],
uint8_t bip32_path_len,
const uint8_t *script,
int32_t script_len);

bool get_core_redeem_script( uint32_t locktime, uint8_t redeem_script[static REDEEM_SCRIPT_LEN]);
void buffer_to_hex(const uint8_t *buffer, size_t buffer_len, char *out, size_t out_len);

void buffer_to_hex(uint8_t *buffer, size_t buffer_len, char *out, size_t out_len);
void format_address(const uint8_t *buffer, size_t buffer_len, char *out, size_t out_len);
Loading