Skip to content

Commit

Permalink
Merge pull request #32 from patnorris/29-secure-send-btc-endpoint
Browse files Browse the repository at this point in the history
29 secure send btc endpoint
  • Loading branch information
patnorris authored Feb 22, 2024
2 parents 9d99de1 + 0d22952 commit 4206e2d
Show file tree
Hide file tree
Showing 26 changed files with 8,627 additions and 119 deletions.
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,16 @@ all-deploy-and-pytest:

cd backend/donation_canister && \
dfx deploy donation_canister --argument '(variant { regtest })' && \
dfx canister update-settings --add-controller be2us-64aaa-aaaaa-qaabq-cai donation_canister && \
donation_canister_id=$$(dfx canister id donation_canister) && \
echo "donation_canister_id: $${donation_canister_id}" && \
argument_string=$$'("'$${donation_canister_id}'")' && \
echo "argument_string: $${argument_string}" && \
cd ../donation_tracker_canister && \
dfx deploy donation_tracker_canister --argument $${argument_string} && \
dfx canister call donation_tracker_canister initRecipients
dfx canister call donation_tracker_canister initRecipients && \
donation_tracker_canister_id=$$(dfx canister id donation_tracker_canister) && \
echo "donation_tracker_canister_id: $${donation_tracker_canister_id}"

pytest

Expand All @@ -95,6 +98,13 @@ all-deploy-and-pytest:
-datadir=$(CURDIR)/bitcoin-$(VERSION_BITCOIN)/data \
stop

.PHONY: bitcoin-core-start
bitcoin-core-start:
bitcoin-$(VERSION_BITCOIN)/bin/bitcoind \
-conf=$(CURDIR)/bitcoin-$(VERSION_BITCOIN)/bitcoin.conf \
-datadir=$(CURDIR)/bitcoin-$(VERSION_BITCOIN)/data \
--port=18444

.PHONY: bitcoin-core-stop
bitcoin-core-stop:
bitcoin-$(VERSION_BITCOIN)/bin/bitcoin-cli \
Expand Down Expand Up @@ -149,7 +159,7 @@ install-bitcoin-core:
# Make sure to source ~/.profile afterwards -> it adds ~/bin to the path if it exists
.PHONY: install-dfx
install-dfx:
sh -ci "$$(curl -fsSL https://sdk.dfinity.org/install.sh)"
DFXVM_INIT_YES=true sh -ci "$$(curl -fsSL https://sdk.dfinity.org/install.sh)"

.PHONY: install-didc
install-didc:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

![Alt text](frontend/src/donation_frontend/assets/BitcoinDonationApp_banner.png)

# The bitcoin-core network

To run locally, you must start up your own, local bitcoin-core network.
See instructions in backend/donation_canister/README.md

# The frontend & backend canisters

The dApp consists out of 3 canister, and each canister is treated as a standalone project.
Expand Down
59 changes: 38 additions & 21 deletions backend/donation_canister/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Donation Canister

## References
- Initial version based on: https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin

- Initial version based on: https://github.com/dfinity/examples/tree/master/motoko/basic_bitcoin
- Hackathon PDF refers to:

- https://internetcomputer.org/how-it-works/bitcoin-integration/
- https://internetcomputer.org/docs/current/tutorials/developer-journey/level-4/4.3-ckbtc-and-bitcoin/

Expand All @@ -22,6 +24,15 @@ For deeper understanding of the ICP < > BTC integration, see the IC wiki article
- Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx).
- [Set up a local Bitcoin network](https://internetcomputer.org/docs/current/tutorials/developer-journey/level-4/4.3-ckbtc-and-bitcoin/#setting-up-a-local-bitcoin-network):

NOTE: On mac I have not been able to get it to run, due to security checks...

```bash
# On linux, you can run
make install-bitcoin-core

# then start it with:
make bitcoin-core-start
```

## Step 1: Building and deploying donations canister

Expand All @@ -38,6 +49,7 @@ cd backend/donation_canister
```

Note on the submodule:

- The donation_canister depends on [motoko-bitcoin](https://github.com/tgalal/motoko-bitcoin)
- I added it to the repo as a submodule with:
```bash
Expand All @@ -62,32 +74,38 @@ We use multiple canisters that we deploy separately on a shared local network. C

File: ~/.config/dfx/networks.json
Note: log_level options are: "critical", "error", "warning", "info", "debug", "trace"

```json
{
"local": {
"bitcoin": {
"enabled": true,
"log_level": "error",
"nodes": [
"127.0.0.1:18444"
]
"nodes": ["127.0.0.1:18444"]
}
}
}
```
### Deploy local

### Deploy donation_canister locally

[Reference](https://internetcomputer.org/docs/current/tutorials/developer-journey/level-4/4.3-ckbtc-and-bitcoin/#deploying-the-example-canister)

```bash
# From the bitcoin-core/bitcoin-25.0 folder, start the local bitcoin instance
# From the bitcoin-25.0 folder, start the local bitcoin instance
./bin/bitcoind -conf=$(pwd)/bitcoin.conf -datadir=$(pwd)/data --port=18444
# Start the local dfx network
dfx start --clean
# From the backend/donation_canister folder:
dfx deploy donation_canister --argument '(variant { regtest })'
# Add the donation_tracker_canister as a controller, for access to the `send` method
dfx canister update-settings --add-controller be2us-64aaa-aaaaa-qaabq-cai donation_canister
# Generate the bindings used by the frontend
dfx generate
```

### Exercise it with dfx
Expand All @@ -98,7 +116,7 @@ $ dfx canister call donation_canister get_p2pkh_address
("mkkzk2xTQcrrRYv8FPnj22ujHhkZwsETtX")
# receiving BTC
# From your local bitcoin-core folder, issue this command, replacing BTC_ADDRESS with yours
# From bitcoin-25.0 issue this command, replacing BTC_ADDRESS with yours
./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 1 BTC_ADDRESS
# eg.
$ ./bin/bitcoin-cli -conf=$(pwd)/bitcoin.conf generatetoaddress 1 mkkzk2xTQcrrRYv8FPnj22ujHhkZwsETtX
Expand All @@ -115,22 +133,23 @@ $ dfx canister call donation_canister get_balance '("mkkzk2xTQcrrRYv8FPnj22ujHhk

---

### Deploy the smart contract to the Internet Computer

This is done only once.
### Deploy donation_canister to the Internet Computer

```bash
dfx deploy --network=ic -m reinstall donation_canister --argument '(variant { testnet })'
# Add the donation_tracker_canister as a controller, for access to the `send` method
dfx canister --network=ic update-settings --add-controller fj5jn-2qaaa-aaaag-acmfq-cai donation_canister
```

#### What this does

- `dfx deploy` tells the command line interface to `deploy` the smart contract
- `--network=ic` tells the command line to deploy the smart contract to the mainnet ICP blockchain
- `--argument '(variant { testnet })'` passes the argument `Testnet` to initialize the smart contract, telling it to connect to the Bitcoin testnet

**We're initializing the canister with `variant { testnet }`, so that the canister connects to the the [Bitcoin testnet](https://en.bitcoin.it/wiki/Testnet). To be specific, this connects to `Testnet3`, which is the current Bitcoin test network used by the Bitcoin community.**
If successful, you should see an output that looks like this:
```bash
Expand All @@ -147,8 +166,8 @@ Your canister is live and ready to use! You can interact with it using either th
In the output above, to see the Candid Web UI for your bitcoin canister, you would use the URL `https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=<YOUR-CANISTER-ID>`. Here are the two methods you will see:
* `public_key`
* `sign`
- `public_key`
- `sign`
## Step 2: Generating a Bitcoin address
Expand All @@ -165,12 +184,11 @@ Or, if you prefer the command line:
dfx canister --network=ic call donation_canister get_p2pkh_address
```
* The Bitcoin address you see will be different from the one above, because the
- The Bitcoin address you see will be different from the one above, because the
ECDSA public key your canister retrieves is unique.
* We are generating a Bitcoin testnet address, which can only be
used for sending/receiving Bitcoin on the Bitcoin testnet.
- We are generating a Bitcoin testnet address, which can only be
used for sending/receiving Bitcoin on the Bitcoin testnet.
## Step 3: Receiving bitcoin
Expand All @@ -180,7 +198,6 @@ to receive some bitcoin.

Enter your address and click on "Send testnet bitcoins". In the example below we will use Bitcoin address `n31eU1K11m1r58aJMgTyxGonu7wSMoUYe7`, but you would use your own address. The canister will be receiving 0.011 test BTC on the Bitcoin Testnet.


Once the transaction has at least one confirmation, which can take a few minutes,
you'll be able to see it in your canister's balance.

Expand Down Expand Up @@ -225,11 +242,11 @@ You can track the status of this transaction using a block explorer. Once the
transaction has at least one confirmation, you should be able to see it
reflected in your current balance.


## Security considerations and best practices

If you base your application on this example, we recommend you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/current/references/security/) for developing on the Internet Computer. This example may not implement all the best practices.

For example, the following aspects are particularly relevant for this app:
* [Certify query responses if they are relevant for security](https://internetcomputer.org/docs/current/references/security/general-security-best-practices#certify-query-responses-if-they-are-relevant-for-security), since the app e.g. offers method to read balances.
* [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://internetcomputer.org/docs/current/references/security/rust-canister-development-security-best-practices#use-a-decentralized-governance-system-like-sns-to-make-a-canister-have-a-decentralized-controller), since decentralized control may be essential for canisters holding Bitcoin on behalf of users.

- [Certify query responses if they are relevant for security](https://internetcomputer.org/docs/current/references/security/general-security-best-practices#certify-query-responses-if-they-are-relevant-for-security), since the app e.g. offers method to read balances.
- [Use a decentralized governance system like SNS to make a canister have a decentralized controller](https://internetcomputer.org/docs/current/references/security/rust-canister-development-security-best-practices#use-a-decentralized-governance-system-like-sns-to-make-a-canister-have-a-decentralized-controller), since decentralized control may be essential for canisters holding Bitcoin on behalf of users.
3 changes: 2 additions & 1 deletion backend/donation_canister/canister_ids.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"donation_canister": {
"ic": "ekral-oiaaa-aaaag-acmda-cai"
"ic": "ekral-oiaaa-aaaag-acmda-cai",
"local": "bkyz2-fmaaa-aaaaa-qaaaq-cai"
}
}
14 changes: 11 additions & 3 deletions backend/donation_canister/src/Main.mo
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,16 @@ actor class BasicBitcoin(_network : Types.Network) {
};

/// Sends the given amount of bitcoin from this canister to the given address.
/// Returns the transaction ID.
public func send(request : SendRequest) : async Text {
Utils.bytesToText(await BitcoinWallet.send(NETWORK, DERIVATION_PATH, KEY_NAME, request.destination_address, request.amount_in_satoshi));
/// Only controllers of this canister allowed to call.
/// - Note that the donation_tracker_canister will be a controller.
/// Returns a SendRecordResult, containing the transaction ID
public shared (msg) func send(request : SendRequest) : async Types.SendRecordResult {
if (not Principal.isController(msg.caller)) {
return #Err(#Unauthorized);
};
let txid = Utils.bytesToText(await BitcoinWallet.send(NETWORK, DERIVATION_PATH, KEY_NAME, request.destination_address, request.amount_in_satoshi));

let sendRecord = { txid = txid };
return #Ok(sendRecord);
};
};
8 changes: 8 additions & 0 deletions backend/donation_canister/src/Types.mo
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ module Types {
amount_in_satoshi : Satoshi;
};

//-------------------------------------------------------------------------
public type SendRecord = {
txid : Text;
};

public type SendRecordResult = Result<SendRecord, ApiError>;

//-------------------------------------------------------------------------
public type ECDSAPublicKeyReply = {
public_key : Blob;
chain_code : Blob;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
type Utxo =
record {
height: nat32;
outpoint: OutPoint;
value: Satoshi;
};
type SendRequest =
record {
amount_in_satoshi: Satoshi;
destination_address: text;
};
type SendRecordResult =
variant {
Err: ApiError;
Ok: SendRecord;
};
type SendRecord = record {txid: text;};
type Satoshi__1 = nat64;
type Satoshi = nat64;
type Page = vec nat8;
type OutPoint =
record {
txid: blob;
vout: nat32;
};
type Network =
variant {
mainnet;
regtest;
testnet;
};
type MillisatoshiPerVByte = nat64;
type GetUtxosResponse =
record {
next_page: opt Page;
tip_block_hash: BlockHash;
tip_height: nat32;
utxos: vec Utxo;
};
type BlockHash = vec nat8;
type BitcoinAddress = text;
type BasicBitcoin =
service {
amiController: () -> (AuthRecordResult);
/// Returns the balance of the given Bitcoin address.
get_balance: (BitcoinAddress) -> (Satoshi__1);
/// Returns the 100 fee percentiles measured in millisatoshi/vbyte.
/// Percentiles are computed from the last 10,000 transactions (if available).
get_current_fee_percentiles: () -> (vec MillisatoshiPerVByte);
/// Returns the P2PKH address of this canister at a specific derivation path.
get_p2pkh_address: () -> (BitcoinAddress);
/// Returns the UTXOs of the given Bitcoin address.
get_utxos: (BitcoinAddress) -> (GetUtxosResponse);
/// Sends the given amount of bitcoin from this canister to the given address.
/// Only controllers of this canister allowed to call.
/// - Note that the donation_tracker_canister will be a controller.
/// Returns a SendRecordResult, containing the transaction ID
send: (SendRequest) -> (SendRecordResult);
whoami: () -> (principal);
};
type AuthRecordResult =
variant {
Err: ApiError;
Ok: AuthRecord;
};
type AuthRecord = record {auth: text;};
type ApiError =
variant {
InvalidId;
Other: text;
Unauthorized;
ZeroAddress;
};
service : (Network) -> BasicBitcoin
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Principal } from '@dfinity/principal';
import type { ActorMethod } from '@dfinity/agent';
import type { IDL } from '@dfinity/candid';

export type ApiError = { 'InvalidId' : null } |
{ 'ZeroAddress' : null } |
{ 'Unauthorized' : null } |
{ 'Other' : string };
export interface AuthRecord { 'auth' : string }
export type AuthRecordResult = { 'Ok' : AuthRecord } |
{ 'Err' : ApiError };
export interface BasicBitcoin {
'amiController' : ActorMethod<[], AuthRecordResult>,
'get_balance' : ActorMethod<[BitcoinAddress], Satoshi__1>,
'get_current_fee_percentiles' : ActorMethod<[], BigUint64Array | bigint[]>,
'get_p2pkh_address' : ActorMethod<[], BitcoinAddress>,
'get_utxos' : ActorMethod<[BitcoinAddress], GetUtxosResponse>,
'send' : ActorMethod<[SendRequest], SendRecordResult>,
'whoami' : ActorMethod<[], Principal>,
}
export type BitcoinAddress = string;
export type BlockHash = Uint8Array | number[];
export interface GetUtxosResponse {
'next_page' : [] | [Page],
'tip_height' : number,
'tip_block_hash' : BlockHash,
'utxos' : Array<Utxo>,
}
export type MillisatoshiPerVByte = bigint;
export type Network = { 'mainnet' : null } |
{ 'regtest' : null } |
{ 'testnet' : null };
export interface OutPoint { 'txid' : Uint8Array | number[], 'vout' : number }
export type Page = Uint8Array | number[];
export type Satoshi = bigint;
export type Satoshi__1 = bigint;
export interface SendRecord { 'txid' : string }
export type SendRecordResult = { 'Ok' : SendRecord } |
{ 'Err' : ApiError };
export interface SendRequest {
'destination_address' : string,
'amount_in_satoshi' : Satoshi,
}
export interface Utxo {
'height' : number,
'value' : Satoshi,
'outpoint' : OutPoint,
}
export interface _SERVICE extends BasicBitcoin {}
export declare const idlFactory: IDL.InterfaceFactory;
export declare const init: ({ IDL }: { IDL: IDL }) => IDL.Type[];
Loading

0 comments on commit 4206e2d

Please sign in to comment.