@getcofund/protocol 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +414 -2
- package/dist/btc/index.d.ts +12 -0
- package/dist/btc/index.js +13 -0
- package/dist/btc/psbt.d.ts +121 -0
- package/dist/btc/psbt.js +377 -0
- package/dist/btc/reader.d.ts +42 -0
- package/dist/btc/reader.js +85 -0
- package/dist/btc/select.d.ts +48 -0
- package/dist/btc/select.js +59 -0
- package/dist/btc/spend-rules.d.ts +24 -0
- package/dist/btc/spend-rules.js +43 -0
- package/dist/btc/spend.d.ts +136 -0
- package/dist/btc/spend.js +180 -0
- package/dist/btc/types.d.ts +65 -0
- package/dist/btc/types.js +62 -0
- package/dist/btc/vault/bip341-nums.d.ts +35 -0
- package/dist/btc/vault/bip341-nums.js +121 -0
- package/dist/btc/vault/build.d.ts +5 -0
- package/dist/btc/vault/build.js +136 -0
- package/dist/btc/vault/canonical-order.d.ts +10 -0
- package/dist/btc/vault/canonical-order.js +18 -0
- package/dist/btc/vault/from-contract-id.d.ts +9 -0
- package/dist/btc/vault/from-contract-id.js +74 -0
- package/dist/btc/vault/helpers.d.ts +73 -0
- package/dist/btc/vault/helpers.js +129 -0
- package/dist/btc/vault/index.d.ts +21 -0
- package/dist/btc/vault/index.js +21 -0
- package/dist/btc/vault/limits.d.ts +9 -0
- package/dist/btc/vault/limits.js +9 -0
- package/dist/btc/vault/participant-order.d.ts +69 -0
- package/dist/btc/vault/participant-order.js +135 -0
- package/dist/btc/vault/script.d.ts +13 -0
- package/dist/btc/vault/script.js +39 -0
- package/dist/btc/vault/spend.d.ts +63 -0
- package/dist/btc/vault/spend.js +93 -0
- package/dist/btc/vault/tx-size.d.ts +66 -0
- package/dist/btc/vault/tx-size.js +71 -0
- package/dist/btc/vault/types.d.ts +49 -0
- package/dist/btc/vault/types.js +1 -0
- package/dist/btc/vault/vault-policy.d.ts +109 -0
- package/dist/btc/vault/vault-policy.js +213 -0
- package/dist/btc/vault/vaultContractUtil.d.ts +80 -0
- package/dist/btc/vault/vaultContractUtil.js +87 -0
- package/dist/btc/vault/xpub.d.ts +28 -0
- package/dist/btc/vault/xpub.js +41 -0
- package/dist/btc/witness.d.ts +101 -0
- package/dist/btc/witness.js +439 -0
- package/dist/btc-policies/activation.d.ts +76 -0
- package/dist/btc-policies/activation.js +153 -0
- package/dist/btc-policies/index.d.ts +7 -0
- package/dist/btc-policies/index.js +8 -0
- package/dist/btc-policies/policy.d.ts +55 -0
- package/dist/btc-policies/policy.js +210 -0
- package/dist/evm/abis/index.d.ts +10 -0
- package/dist/evm/abis/index.js +11 -0
- package/dist/evm/abis/policies.d.ts +279 -0
- package/dist/evm/abis/policies.js +363 -0
- package/dist/evm/abis/sponsor-guard.d.ts +201 -0
- package/dist/evm/abis/sponsor-guard.js +263 -0
- package/dist/evm/abis/state.d.ts +989 -0
- package/dist/evm/abis/state.js +1290 -0
- package/dist/evm/abis/users.d.ts +408 -0
- package/dist/evm/abis/users.js +532 -0
- package/dist/evm/abis/vault-factory.d.ts +92 -0
- package/dist/evm/abis/vault-factory.js +126 -0
- package/dist/evm/abis/vault.d.ts +343 -0
- package/dist/evm/abis/vault.js +444 -0
- package/dist/evm/address.d.ts +2 -0
- package/dist/evm/address.js +4 -0
- package/dist/evm/calls/common.d.ts +30 -0
- package/dist/evm/calls/common.js +51 -0
- package/dist/evm/calls/index.d.ts +11 -0
- package/dist/evm/calls/index.js +4 -0
- package/dist/evm/calls/policies.d.ts +25 -0
- package/dist/evm/calls/policies.js +173 -0
- package/dist/evm/calls/users.d.ts +61 -0
- package/dist/evm/calls/users.js +353 -0
- package/dist/evm/calls/vault.d.ts +25 -0
- package/dist/evm/calls/vault.js +184 -0
- package/dist/evm/chain.d.ts +74 -0
- package/dist/evm/chain.js +48 -0
- package/dist/evm/company-name.d.ts +46 -0
- package/dist/evm/company-name.js +64 -0
- package/dist/evm/deployment.d.ts +47 -0
- package/dist/evm/deployment.js +110 -0
- package/dist/evm/eip712.d.ts +271 -0
- package/dist/evm/eip712.js +224 -0
- package/dist/evm/index.d.ts +20 -0
- package/dist/evm/index.js +21 -0
- package/dist/evm/reads.d.ts +64 -0
- package/dist/evm/reads.js +131 -0
- package/dist/evm/signatures.d.ts +57 -0
- package/dist/evm/signatures.js +105 -0
- package/dist/evm/simulate/balances.d.ts +76 -0
- package/dist/evm/simulate/balances.js +201 -0
- package/dist/evm/simulate/describe.d.ts +79 -0
- package/dist/evm/simulate/describe.js +188 -0
- package/dist/evm/simulate/index.d.ts +12 -0
- package/dist/evm/simulate/index.js +13 -0
- package/dist/evm/simulate/own-node.d.ts +62 -0
- package/dist/evm/simulate/own-node.js +122 -0
- package/dist/evm/simulate/request.d.ts +86 -0
- package/dist/evm/simulate/request.js +235 -0
- package/dist/evm/simulate/revert.d.ts +29 -0
- package/dist/evm/simulate/revert.js +192 -0
- package/dist/evm/simulate/run.d.ts +51 -0
- package/dist/evm/simulate/run.js +214 -0
- package/dist/evm/stocks/decode-stock-swap.d.ts +32 -0
- package/dist/evm/stocks/decode-stock-swap.js +79 -0
- package/dist/evm/stocks/encode-swap.d.ts +45 -0
- package/dist/evm/stocks/encode-swap.js +45 -0
- package/dist/evm/stocks/index.d.ts +9 -0
- package/dist/evm/stocks/index.js +10 -0
- package/dist/evm/stocks/quote.d.ts +19 -0
- package/dist/evm/stocks/quote.js +46 -0
- package/dist/evm/stocks/stock-pools.d.ts +61 -0
- package/dist/evm/stocks/stock-pools.js +179 -0
- package/dist/evm/submitters.d.ts +32 -0
- package/dist/evm/submitters.js +105 -0
- package/dist/evm/types.d.ts +146 -0
- package/dist/evm/types.js +128 -0
- package/dist/keys/index.d.ts +6 -0
- package/dist/keys/index.js +7 -0
- package/dist/keys/keys.d.ts +63 -0
- package/dist/keys/keys.js +80 -0
- package/dist/keys/xpub-ownership.d.ts +10 -0
- package/dist/keys/xpub-ownership.js +13 -0
- package/package.json +60 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { BtcLeafKeySigner, BtcNetwork } from "../keys/keys.ts";
|
|
2
|
+
import type { BtcPartial, BtcVaultDefinition } from "./types.ts";
|
|
3
|
+
import { type Leaf, type VaultSpendInput } from "./vault/index.ts";
|
|
4
|
+
/**
|
|
5
|
+
* A Bitcoin transfer signed with nothing the server states taken on trust.
|
|
6
|
+
*
|
|
7
|
+
* The vault comes first (`verifyBtcVault`): its taproot tree is rebuilt from
|
|
8
|
+
* its definition with the app's own builder (`./vault`) and must
|
|
9
|
+
* derive the vault's address; the signer's leaves and their derivation
|
|
10
|
+
* branches are read off that tree. Then the spend (`checkBtcSpend`):
|
|
11
|
+
*
|
|
12
|
+
* - the transaction is rebuilt here, with the builder the app and the Ledger
|
|
13
|
+
* path use (`buildVaultSpend`), with the vault's script and the stated
|
|
14
|
+
* amount as every input's prevout, which BIP-341 commits to: an input that
|
|
15
|
+
* is not the vault's, or a false amount, makes every signature over it
|
|
16
|
+
* invalid; no input appears twice;
|
|
17
|
+
* - the first output pays the request's recipient its amount; then at most a
|
|
18
|
+
* Cofund fee within `maxFeeBps` of the amount, and change back to the vault;
|
|
19
|
+
* - the network fee (inputs less outputs) is the request's and at most
|
|
20
|
+
* `maxFeeRate` sat/vB of the transaction's size as the app prices it: every
|
|
21
|
+
* input witnessed by the vault's largest leaf.
|
|
22
|
+
*/
|
|
23
|
+
/** Ceilings on what a spend may cost beyond the transfer itself. */
|
|
24
|
+
export type BtcSpendLimits = {
|
|
25
|
+
/** The Cofund fee's ceiling, in bps of the amount (default 100). */
|
|
26
|
+
maxFeeBps?: number;
|
|
27
|
+
/** The network fee's ceiling, in sat/vB (default 1000). */
|
|
28
|
+
maxFeeRate?: number;
|
|
29
|
+
};
|
|
30
|
+
/** A Bitcoin vault to verify: its address and the definition it claims; `id` names it. */
|
|
31
|
+
export type BtcVault = {
|
|
32
|
+
id: string;
|
|
33
|
+
address: string;
|
|
34
|
+
definition: BtcVaultDefinition;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A transfer out of a vault: what it pays, and the spend that carries it.
|
|
38
|
+
* Every input is the vault's; a signer signs it as the vault's script
|
|
39
|
+
* spending them.
|
|
40
|
+
*/
|
|
41
|
+
export type BtcTransfer = {
|
|
42
|
+
vault: {
|
|
43
|
+
id: string;
|
|
44
|
+
address: string;
|
|
45
|
+
};
|
|
46
|
+
recipient: string;
|
|
47
|
+
/** Sats. */
|
|
48
|
+
amount: string;
|
|
49
|
+
/** Sats: the inputs less the outputs. */
|
|
50
|
+
networkFee: string;
|
|
51
|
+
spend: {
|
|
52
|
+
/** The vault's UTXOs, by id. */
|
|
53
|
+
inputs: {
|
|
54
|
+
utxoId: string;
|
|
55
|
+
txid: string;
|
|
56
|
+
vout: number;
|
|
57
|
+
amount: string;
|
|
58
|
+
}[];
|
|
59
|
+
/** The recipient, then the Cofund fee if any, then change back to the vault if any. */
|
|
60
|
+
outputs: {
|
|
61
|
+
address: string;
|
|
62
|
+
amount: string;
|
|
63
|
+
}[];
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/** A vault whose definition derives its address: its tree, and each leaf's derivation branch. */
|
|
67
|
+
export type VerifiedBtcVault = {
|
|
68
|
+
id: string;
|
|
69
|
+
address: string;
|
|
70
|
+
definition: BtcVaultDefinition;
|
|
71
|
+
/** sha256 of the definition as the app serializes it: what a keystore pins (`BtcVaultPin`). */
|
|
72
|
+
definitionHash: string;
|
|
73
|
+
script: Uint8Array;
|
|
74
|
+
leaves: {
|
|
75
|
+
leaf: Leaf;
|
|
76
|
+
branch: number;
|
|
77
|
+
}[];
|
|
78
|
+
};
|
|
79
|
+
/** What a checked spend pays, and the vault it spends, for a person or a hook to decide on. */
|
|
80
|
+
export type BtcSpendSummary = {
|
|
81
|
+
vault: {
|
|
82
|
+
address: string;
|
|
83
|
+
vaultType: BtcVaultDefinition["vaultType"];
|
|
84
|
+
threshold: number;
|
|
85
|
+
members: {
|
|
86
|
+
id: string;
|
|
87
|
+
role: "admin" | "member";
|
|
88
|
+
}[];
|
|
89
|
+
};
|
|
90
|
+
recipient: string;
|
|
91
|
+
amount: string;
|
|
92
|
+
/** The Cofund fee output, when the spend carries one. */
|
|
93
|
+
cofundFee: {
|
|
94
|
+
address: string;
|
|
95
|
+
amount: string;
|
|
96
|
+
} | null;
|
|
97
|
+
/** Change back to the vault, in sats. */
|
|
98
|
+
change: string;
|
|
99
|
+
networkFee: string;
|
|
100
|
+
inputs: number;
|
|
101
|
+
};
|
|
102
|
+
/** A spend that passed every check, ready to sign. */
|
|
103
|
+
export type CheckedBtcSpend = {
|
|
104
|
+
vaultScript: Uint8Array;
|
|
105
|
+
/** In the order signed (`vaultSpendInputOrder`). */
|
|
106
|
+
inputs: (VaultSpendInput & {
|
|
107
|
+
amount: bigint;
|
|
108
|
+
})[];
|
|
109
|
+
outputs: {
|
|
110
|
+
script: Uint8Array;
|
|
111
|
+
amount: bigint;
|
|
112
|
+
}[];
|
|
113
|
+
/** The signer's leaves in the verified tree. */
|
|
114
|
+
leaves: {
|
|
115
|
+
leaf: Leaf;
|
|
116
|
+
branch: number;
|
|
117
|
+
}[];
|
|
118
|
+
summary: BtcSpendSummary;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The vault, verified: its definition rebuilt into the taproot tree with the
|
|
122
|
+
* app's builder, which must derive the vault's address. Keep it for the
|
|
123
|
+
* vault's next transfer: a definition never changes.
|
|
124
|
+
*/
|
|
125
|
+
export declare function verifyBtcVault(vault: BtcVault, network: BtcNetwork): VerifiedBtcVault;
|
|
126
|
+
/**
|
|
127
|
+
* Check a BTC transfer's spend of a verified vault for `signer` (see the
|
|
128
|
+
* module comment). Throws on the first thing that does not hold.
|
|
129
|
+
*/
|
|
130
|
+
export declare function checkBtcSpend(request: BtcTransfer, vault: VerifiedBtcVault, signer: BtcLeafKeySigner, limits?: BtcSpendLimits): CheckedBtcSpend;
|
|
131
|
+
/**
|
|
132
|
+
* The signer's partials over a checked spend: one BIP-340 signature per input
|
|
133
|
+
* per leaf, over the BIP-341 sighash (SIGHASH_DEFAULT, `vaultSpendSighash`)
|
|
134
|
+
* of the transaction `buildVaultSpend` rebuilds.
|
|
135
|
+
*/
|
|
136
|
+
export declare function signBtcSpend(checked: CheckedBtcSpend, signer: BtcLeafKeySigner): BtcPartial[];
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2";
|
|
2
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils";
|
|
3
|
+
import { equalBytes } from "@scure/btc-signer/utils.js";
|
|
4
|
+
import { DEFAULT_MAX_FEE_RATE, networkOfAddress, scriptOf, spendVbytes, } from "./spend-rules.js";
|
|
5
|
+
import { buildLeafBranchMap, buildTaprootForNetwork, buildVaultSpend, serializeAccountBasedData, vaultSpendInputOrder, vaultSpendSighash, } from "./vault/index.js";
|
|
6
|
+
const DEFAULT_MAX_FEE_BPS = 100;
|
|
7
|
+
/** The vault's network, from its address; it must be the signer's. */
|
|
8
|
+
function networkOf(vaultAddress, network) {
|
|
9
|
+
const net = networkOfAddress(vaultAddress);
|
|
10
|
+
if (!net || (net.bech32 === "bc") !== (network === "mainnet")) {
|
|
11
|
+
throw new Error(`The vault ${vaultAddress} is not on the signer's network (${network})`);
|
|
12
|
+
}
|
|
13
|
+
return net;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The vault, verified: its definition rebuilt into the taproot tree with the
|
|
17
|
+
* app's builder, which must derive the vault's address. Keep it for the
|
|
18
|
+
* vault's next transfer: a definition never changes.
|
|
19
|
+
*/
|
|
20
|
+
export function verifyBtcVault(vault, network) {
|
|
21
|
+
const { definition } = vault;
|
|
22
|
+
const net = networkOf(vault.address, network);
|
|
23
|
+
if ((definition.network === "mainnet") !== (network === "mainnet")) {
|
|
24
|
+
throw new Error(`Vault ${vault.id}'s definition is for ${definition.network}`);
|
|
25
|
+
}
|
|
26
|
+
const { members, ...rest } = definition;
|
|
27
|
+
const serialized = serializeAccountBasedData({
|
|
28
|
+
accountBased: true,
|
|
29
|
+
...rest,
|
|
30
|
+
companyUsers: members,
|
|
31
|
+
});
|
|
32
|
+
const built = buildTaprootForNetwork(serialized, net);
|
|
33
|
+
if (built.address !== vault.address) {
|
|
34
|
+
throw new Error(`Vault ${vault.id}'s definition derives ${built.address}, not its address ${vault.address}`);
|
|
35
|
+
}
|
|
36
|
+
const branches = buildLeafBranchMap(built.leaves, definition.vaultType, members, definition.participantOrdering);
|
|
37
|
+
return {
|
|
38
|
+
id: vault.id,
|
|
39
|
+
address: vault.address,
|
|
40
|
+
definition,
|
|
41
|
+
definitionHash: bytesToHex(sha256(utf8ToBytes(serialized))),
|
|
42
|
+
script: built.scriptPubKey,
|
|
43
|
+
leaves: built.leaves.flatMap((leaf) => {
|
|
44
|
+
const branch = branches.get(leaf);
|
|
45
|
+
return branch === undefined ? [] : [{ leaf, branch }];
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The inputs in the order every party signs (`vaultSpendInputOrder`). */
|
|
50
|
+
function checkInputs(request) {
|
|
51
|
+
const { inputs } = request.spend;
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const input of inputs) {
|
|
54
|
+
for (const key of [input.utxoId, `${input.txid}:${input.vout}`]) {
|
|
55
|
+
if (seen.has(key)) {
|
|
56
|
+
throw new Error(`The spend lists ${key} twice`);
|
|
57
|
+
}
|
|
58
|
+
seen.add(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return vaultSpendInputOrder(inputs.map(({ utxoId, txid, vout, amount }) => ({
|
|
62
|
+
id: utxoId,
|
|
63
|
+
txid,
|
|
64
|
+
vout,
|
|
65
|
+
amount: BigInt(amount),
|
|
66
|
+
})));
|
|
67
|
+
}
|
|
68
|
+
/** The Cofund fee's ceiling: `bps` of the amount, rounded up. */
|
|
69
|
+
const feeCeiling = (amount, bps) => (amount * BigInt(bps) + 9999n) / 10000n;
|
|
70
|
+
/** Recipient first, then at most a Cofund fee, then at most change to the vault. */
|
|
71
|
+
function checkOutputs(request, net, vaultScript, maxFeeBps) {
|
|
72
|
+
const [recipient, ...rest] = request.spend.outputs;
|
|
73
|
+
if (recipient?.address !== request.recipient ||
|
|
74
|
+
recipient.amount !== request.amount) {
|
|
75
|
+
throw new Error(`The spend's first output is not ${request.amount} sats to ${request.recipient}`);
|
|
76
|
+
}
|
|
77
|
+
const outputs = [
|
|
78
|
+
{
|
|
79
|
+
script: scriptOf(net, recipient.address),
|
|
80
|
+
amount: BigInt(recipient.amount),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
let cofundFee = null;
|
|
84
|
+
let change = null;
|
|
85
|
+
for (const output of rest) {
|
|
86
|
+
const script = scriptOf(net, output.address);
|
|
87
|
+
const amount = BigInt(output.amount);
|
|
88
|
+
if (change !== null) {
|
|
89
|
+
throw new Error(`Unexpected output after the change: ${output.address}`);
|
|
90
|
+
}
|
|
91
|
+
if (equalBytes(script, vaultScript)) {
|
|
92
|
+
change = amount;
|
|
93
|
+
}
|
|
94
|
+
else if (cofundFee === null) {
|
|
95
|
+
if (amount > feeCeiling(BigInt(request.amount), maxFeeBps)) {
|
|
96
|
+
throw new Error(`The Cofund fee of ${amount} sats is over ${maxFeeBps} bps of the amount`);
|
|
97
|
+
}
|
|
98
|
+
cofundFee = { address: output.address, amount: output.amount };
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw new Error(`Unexpected output: ${amount} sats to ${output.address}`);
|
|
102
|
+
}
|
|
103
|
+
outputs.push({ script, amount });
|
|
104
|
+
}
|
|
105
|
+
return { outputs, cofundFee, change: change ?? 0n };
|
|
106
|
+
}
|
|
107
|
+
/** The signer's leaves: those of the verified tree that hold its key at their branch. */
|
|
108
|
+
function signerLeaves(vault, signer) {
|
|
109
|
+
const mine = vault.leaves.filter(({ leaf, branch }) => {
|
|
110
|
+
const own = signer.leafKey(branch);
|
|
111
|
+
return leaf.signers.some((member) => equalBytes(member.xonly, own));
|
|
112
|
+
});
|
|
113
|
+
if (mine.length === 0) {
|
|
114
|
+
throw new Error(`Vault ${vault.address} holds no leaf with the signer's key`);
|
|
115
|
+
}
|
|
116
|
+
return mine;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Check a BTC transfer's spend of a verified vault for `signer` (see the
|
|
120
|
+
* module comment). Throws on the first thing that does not hold.
|
|
121
|
+
*/
|
|
122
|
+
export function checkBtcSpend(request, vault, signer, limits = {}) {
|
|
123
|
+
if (request.vault.id !== vault.id ||
|
|
124
|
+
request.vault.address !== vault.address) {
|
|
125
|
+
throw new Error(`The transfer spends ${request.vault.address}, not the verified vault ${vault.address}`);
|
|
126
|
+
}
|
|
127
|
+
const maxFeeBps = limits.maxFeeBps ?? DEFAULT_MAX_FEE_BPS;
|
|
128
|
+
const maxFeeRate = limits.maxFeeRate ?? DEFAULT_MAX_FEE_RATE;
|
|
129
|
+
const net = networkOf(vault.address, signer.network);
|
|
130
|
+
const inputs = checkInputs(request);
|
|
131
|
+
const { outputs, cofundFee, change } = checkOutputs(request, net, vault.script, maxFeeBps);
|
|
132
|
+
const leaves = signerLeaves(vault, signer);
|
|
133
|
+
const fee = inputs.reduce((sum, { amount }) => sum + amount, 0n) -
|
|
134
|
+
outputs.reduce((sum, { amount }) => sum + amount, 0n);
|
|
135
|
+
if (fee !== BigInt(request.networkFee)) {
|
|
136
|
+
throw new Error(`Inputs less outputs is ${fee} sats, not the transfer's network fee of ${request.networkFee}`);
|
|
137
|
+
}
|
|
138
|
+
const vsize = spendVbytes(vault, inputs.length, outputs);
|
|
139
|
+
if (fee > BigInt(maxFeeRate * vsize)) {
|
|
140
|
+
throw new Error(`The network fee of ${fee} sats is over ${maxFeeRate} sat/vB of ${vsize} vB`);
|
|
141
|
+
}
|
|
142
|
+
const { definition } = vault;
|
|
143
|
+
return {
|
|
144
|
+
vaultScript: vault.script,
|
|
145
|
+
inputs,
|
|
146
|
+
outputs,
|
|
147
|
+
leaves,
|
|
148
|
+
summary: {
|
|
149
|
+
vault: {
|
|
150
|
+
address: vault.address,
|
|
151
|
+
vaultType: definition.vaultType,
|
|
152
|
+
threshold: definition.threshold,
|
|
153
|
+
members: definition.members.map(({ id, role }) => ({ id, role })),
|
|
154
|
+
},
|
|
155
|
+
recipient: request.recipient,
|
|
156
|
+
amount: request.amount,
|
|
157
|
+
cofundFee,
|
|
158
|
+
change: change.toString(),
|
|
159
|
+
networkFee: fee.toString(),
|
|
160
|
+
inputs: inputs.length,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The signer's partials over a checked spend: one BIP-340 signature per input
|
|
166
|
+
* per leaf, over the BIP-341 sighash (SIGHASH_DEFAULT, `vaultSpendSighash`)
|
|
167
|
+
* of the transaction `buildVaultSpend` rebuilds.
|
|
168
|
+
*/
|
|
169
|
+
export function signBtcSpend(checked, signer) {
|
|
170
|
+
const { tx, inputs } = buildVaultSpend({
|
|
171
|
+
vaultScript: checked.vaultScript,
|
|
172
|
+
inputs: checked.inputs,
|
|
173
|
+
outputs: checked.outputs,
|
|
174
|
+
});
|
|
175
|
+
return checked.leaves.flatMap(({ leaf, branch }) => inputs.map((input, index) => ({
|
|
176
|
+
utxoId: input.id,
|
|
177
|
+
leafHash: bytesToHex(leaf.leafHash),
|
|
178
|
+
signature: bytesToHex(signer.signDigest(branch, vaultSpendSighash(tx, index, leaf.script))),
|
|
179
|
+
})));
|
|
180
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the protocol's Bitcoin values look like, as zod schemas: a vault's
|
|
3
|
+
* definition and a signer's partials over a spend. Every type is inferred
|
|
4
|
+
* from them.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
/** Every id: cofund, user, policy, vault, request, and a signed `authId`, `callerId` or `userId`. */
|
|
8
|
+
export declare const Uuid: z.ZodUUID;
|
|
9
|
+
/** A 64-byte BIP-340 signature, bare hex. */
|
|
10
|
+
export declare const SchnorrSignature: z.ZodString;
|
|
11
|
+
/** A BIP-86 account key: an xpub, or a tpub off mainnet. */
|
|
12
|
+
export declare const BtcXpub: z.ZodString;
|
|
13
|
+
/** A tapleaf hash: 32 bytes, bare hex. */
|
|
14
|
+
export declare const TapLeafHash: z.ZodString;
|
|
15
|
+
/** A Bitcoin vault's template: everyone equal, admins among the members, or an admin on every spend. */
|
|
16
|
+
export declare const BtcVaultTypeSchema: z.ZodEnum<{
|
|
17
|
+
ALL: "ALL";
|
|
18
|
+
ADMINS_INCLUDED: "ADMINS_INCLUDED";
|
|
19
|
+
ADMINS_REQUIRED: "ADMINS_REQUIRED";
|
|
20
|
+
}>;
|
|
21
|
+
export type BtcVaultType = z.infer<typeof BtcVaultTypeSchema>;
|
|
22
|
+
/**
|
|
23
|
+
* What a Bitcoin vault's address commits to, as the app stores it: the
|
|
24
|
+
* template, the threshold, the rule its members are ordered under, the NUMS
|
|
25
|
+
* recipe of its internal key, and each member's account xpub. `verifyBtcVault`
|
|
26
|
+
* rebuilds the taproot tree from it with the vault builder (`./vault`) and
|
|
27
|
+
* checks the address.
|
|
28
|
+
*/
|
|
29
|
+
export declare const BtcVaultDefinitionSchema: z.ZodObject<{
|
|
30
|
+
vaultType: z.ZodEnum<{
|
|
31
|
+
ALL: "ALL";
|
|
32
|
+
ADMINS_INCLUDED: "ADMINS_INCLUDED";
|
|
33
|
+
ADMINS_REQUIRED: "ADMINS_REQUIRED";
|
|
34
|
+
}>;
|
|
35
|
+
network: z.ZodEnum<{
|
|
36
|
+
testnet: "testnet";
|
|
37
|
+
mainnet: "mainnet";
|
|
38
|
+
}>;
|
|
39
|
+
threshold: z.ZodNumber;
|
|
40
|
+
participantOrdering: z.ZodEnum<{
|
|
41
|
+
"icu-en-v1": "icu-en-v1";
|
|
42
|
+
"role-then-xpub-codepoint-v2": "role-then-xpub-codepoint-v2";
|
|
43
|
+
}>;
|
|
44
|
+
nums: z.ZodObject<{
|
|
45
|
+
scheme: z.ZodLiteral<"bip341-h-plus-r-bip32-public">;
|
|
46
|
+
version: z.ZodLiteral<1>;
|
|
47
|
+
r: z.ZodString;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
members: z.ZodArray<z.ZodObject<{
|
|
50
|
+
id: z.ZodUUID;
|
|
51
|
+
role: z.ZodEnum<{
|
|
52
|
+
admin: "admin";
|
|
53
|
+
member: "member";
|
|
54
|
+
}>;
|
|
55
|
+
accountXpub: z.ZodString;
|
|
56
|
+
}, z.core.$strip>>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
export type BtcVaultDefinition = z.infer<typeof BtcVaultDefinitionSchema>;
|
|
59
|
+
/** One BIP-340 signature by the caller's key in `leafHash`, over the spend's input `utxoId`. */
|
|
60
|
+
export declare const BtcPartialSchema: z.ZodObject<{
|
|
61
|
+
utxoId: z.ZodUUID;
|
|
62
|
+
leafHash: z.ZodString;
|
|
63
|
+
signature: z.ZodString;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
export type BtcPartial = z.infer<typeof BtcPartialSchema>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the protocol's Bitcoin values look like, as zod schemas: a vault's
|
|
3
|
+
* definition and a signer's partials over a spend. Every type is inferred
|
|
4
|
+
* from them.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { BIP341_NUMS_RECIPE_VERSION, BIP341_NUMS_SCHEME, } from "./vault/bip341-nums.js";
|
|
8
|
+
import { BTC_VAULT_MAX_PARTICIPANTS } from "./vault/limits.js";
|
|
9
|
+
import { ORDERING_ICU_EN_V1, ORDERING_XPUB_CODEPOINT_V2, } from "./vault/participant-order.js";
|
|
10
|
+
/** Every id: cofund, user, policy, vault, request, and a signed `authId`, `callerId` or `userId`. */
|
|
11
|
+
export const Uuid = z.uuid();
|
|
12
|
+
/** A 64-byte BIP-340 signature, bare hex. */
|
|
13
|
+
export const SchnorrSignature = z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(/^[0-9a-fA-F]{128}$/, "a 64-byte BIP-340 signature");
|
|
16
|
+
/** A BIP-86 account key: an xpub, or a tpub off mainnet. */
|
|
17
|
+
export const BtcXpub = z
|
|
18
|
+
.string()
|
|
19
|
+
.regex(/^[xt]pub[1-9A-HJ-NP-Za-km-z]{107}$/, "an xpub or tpub");
|
|
20
|
+
/** A tapleaf hash: 32 bytes, bare hex. */
|
|
21
|
+
export const TapLeafHash = z
|
|
22
|
+
.string()
|
|
23
|
+
.regex(/^[0-9a-f]{64}$/, "a 32-byte tapleaf hash");
|
|
24
|
+
/** A Bitcoin vault's template: everyone equal, admins among the members, or an admin on every spend. */
|
|
25
|
+
export const BtcVaultTypeSchema = z.enum([
|
|
26
|
+
"ALL",
|
|
27
|
+
"ADMINS_INCLUDED",
|
|
28
|
+
"ADMINS_REQUIRED",
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* What a Bitcoin vault's address commits to, as the app stores it: the
|
|
32
|
+
* template, the threshold, the rule its members are ordered under, the NUMS
|
|
33
|
+
* recipe of its internal key, and each member's account xpub. `verifyBtcVault`
|
|
34
|
+
* rebuilds the taproot tree from it with the vault builder (`./vault`) and
|
|
35
|
+
* checks the address.
|
|
36
|
+
*/
|
|
37
|
+
export const BtcVaultDefinitionSchema = z.object({
|
|
38
|
+
vaultType: BtcVaultTypeSchema,
|
|
39
|
+
network: z.enum(["mainnet", "testnet"]),
|
|
40
|
+
/** Signatures the script requires (2 on a vault from before it was configurable). */
|
|
41
|
+
threshold: z.number().int().min(1).max(BTC_VAULT_MAX_PARTICIPANTS),
|
|
42
|
+
participantOrdering: z.enum([ORDERING_ICU_EN_V1, ORDERING_XPUB_CODEPOINT_V2]),
|
|
43
|
+
nums: z.object({
|
|
44
|
+
scheme: z.literal(BIP341_NUMS_SCHEME),
|
|
45
|
+
version: z.literal(BIP341_NUMS_RECIPE_VERSION),
|
|
46
|
+
r: z.string().regex(/^[0-9a-f]{64}$/, "32 bytes of hex"),
|
|
47
|
+
}),
|
|
48
|
+
members: z
|
|
49
|
+
.array(z.object({
|
|
50
|
+
id: Uuid,
|
|
51
|
+
role: z.enum(["admin", "member"]),
|
|
52
|
+
accountXpub: BtcXpub,
|
|
53
|
+
}))
|
|
54
|
+
.min(1)
|
|
55
|
+
.max(BTC_VAULT_MAX_PARTICIPANTS),
|
|
56
|
+
});
|
|
57
|
+
/** One BIP-340 signature by the caller's key in `leafHash`, over the spend's input `utxoId`. */
|
|
58
|
+
export const BtcPartialSchema = z.object({
|
|
59
|
+
utxoId: Uuid,
|
|
60
|
+
leafHash: TapLeafHash,
|
|
61
|
+
signature: SchnorrSignature,
|
|
62
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const BIP341_NUMS_RECIPE_VERSION: 1;
|
|
2
|
+
export declare const BIP341_NUMS_SCHEME: "bip341-h-plus-r-bip32-public";
|
|
3
|
+
export declare const BIP341_NUMS_CHILD_INDEX: 376864517;
|
|
4
|
+
/**
|
|
5
|
+
* Public proof material for Cofund's BIP341 NUMS internal-key construction.
|
|
6
|
+
*
|
|
7
|
+
* `r` is intentionally public. It proves that the synthetic extended
|
|
8
|
+
* public key was constructed from BIP341's standard NUMS point rather than
|
|
9
|
+
* from a public key whose discrete logarithm is known.
|
|
10
|
+
*/
|
|
11
|
+
export type Bip341NumsRecipe = {
|
|
12
|
+
scheme: typeof BIP341_NUMS_SCHEME;
|
|
13
|
+
version: typeof BIP341_NUMS_RECIPE_VERSION;
|
|
14
|
+
r: string;
|
|
15
|
+
};
|
|
16
|
+
export type DerivedBip341NumsKey = {
|
|
17
|
+
rootCompressed: Uint8Array;
|
|
18
|
+
chainCode: Uint8Array;
|
|
19
|
+
extendedPublicKey: string;
|
|
20
|
+
internalCompressed: Uint8Array;
|
|
21
|
+
internalXonly: Uint8Array;
|
|
22
|
+
};
|
|
23
|
+
export type RandomBytesSource = (length: number) => Uint8Array;
|
|
24
|
+
/** Generate fresh, public proof material once during vault creation. */
|
|
25
|
+
export declare function createBip341NumsRecipe(randomSource?: RandomBytesSource): Bip341NumsRecipe;
|
|
26
|
+
/**
|
|
27
|
+
* Reconstruct the public-only extended key used by software and Ledger.
|
|
28
|
+
*
|
|
29
|
+
* Ledger wallet policies currently require an xpub-backed key placeholder and
|
|
30
|
+
* a receive/change derivation suffix. The root point is `H + rG`, where `H` is
|
|
31
|
+
* BIP341's standard NUMS point and `r` is the recipe's public scalar. BIP32
|
|
32
|
+
* public derivation adds only publicly computable point tweaks, so no private
|
|
33
|
+
* key is known unless the discrete logarithm of `H` is known.
|
|
34
|
+
*/
|
|
35
|
+
export declare function deriveBip341NumsKey(recipe: Bip341NumsRecipe, network: "mainnet" | "testnet"): DerivedBip341NumsKey;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
|
|
2
|
+
import { bytesToNumberBE } from "@noble/curves/utils.js";
|
|
3
|
+
import { bytesToHex, hexToBytes, randomBytes } from "@noble/hashes/utils";
|
|
4
|
+
import { HDKey } from "@scure/bip32";
|
|
5
|
+
export const BIP341_NUMS_RECIPE_VERSION = 1;
|
|
6
|
+
export const BIP341_NUMS_SCHEME = "bip341-h-plus-r-bip32-public";
|
|
7
|
+
export const BIP341_NUMS_CHILD_INDEX = 376_864_517;
|
|
8
|
+
const BIP341_NUMS_H_X = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
|
|
9
|
+
const NUMS_CHAIN_CODE_TAG = "Cofund/NUMSXpubChainCode/v1";
|
|
10
|
+
const NUMS_BRANCH = 0;
|
|
11
|
+
const SCALAR_HEX = /^[0-9a-f]{64}$/;
|
|
12
|
+
const CURVE_ORDER = secp256k1.Point.CURVE().n;
|
|
13
|
+
const TESTNET_HD_VERSIONS = {
|
|
14
|
+
public: 0x04_35_87_cf,
|
|
15
|
+
private: 0x04_35_83_94,
|
|
16
|
+
};
|
|
17
|
+
const HD_VERSIONS = {
|
|
18
|
+
mainnet: {
|
|
19
|
+
public: 0x04_88_b2_1e,
|
|
20
|
+
private: 0x04_88_ad_e4,
|
|
21
|
+
},
|
|
22
|
+
testnet: TESTNET_HD_VERSIONS,
|
|
23
|
+
};
|
|
24
|
+
class NonExactBip32DerivationError extends Error {
|
|
25
|
+
}
|
|
26
|
+
/** Generate fresh, public proof material once during vault creation. */
|
|
27
|
+
export function createBip341NumsRecipe(randomSource = randomBytes) {
|
|
28
|
+
// Rejection sampling gives BIP341's exact uniform distribution over 0..n-1
|
|
29
|
+
// recommended by BIP341, without reducing random bytes modulo the order.
|
|
30
|
+
for (let attempt = 0; attempt < 128; attempt++) {
|
|
31
|
+
const candidate = randomSource(32);
|
|
32
|
+
if (candidate.length !== 32) {
|
|
33
|
+
throw new Error("BIP341 NUMS random source must return 32 bytes");
|
|
34
|
+
}
|
|
35
|
+
if (bytesToNumberBE(candidate) < CURVE_ORDER) {
|
|
36
|
+
const recipe = {
|
|
37
|
+
scheme: BIP341_NUMS_SCHEME,
|
|
38
|
+
version: BIP341_NUMS_RECIPE_VERSION,
|
|
39
|
+
r: bytesToHex(candidate),
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
deriveBip341NumsKey(recipe, "mainnet");
|
|
43
|
+
return recipe;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof NonExactBip32DerivationError) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error("Unable to sample a valid BIP341 NUMS scalar");
|
|
54
|
+
}
|
|
55
|
+
/** Validate and decode the public scalar committed by a NUMS recipe. */
|
|
56
|
+
function decodeNumsScalar(recipe) {
|
|
57
|
+
if (recipe.scheme !== BIP341_NUMS_SCHEME) {
|
|
58
|
+
throw new Error(`Unsupported BIP341 NUMS scheme: ${recipe.scheme}`);
|
|
59
|
+
}
|
|
60
|
+
if (recipe.version !== BIP341_NUMS_RECIPE_VERSION) {
|
|
61
|
+
throw new Error(`Unsupported BIP341 NUMS recipe: ${recipe.version}`);
|
|
62
|
+
}
|
|
63
|
+
if (!SCALAR_HEX.test(recipe.r)) {
|
|
64
|
+
throw new Error("BIP341 NUMS r must be 32-byte lowercase hex");
|
|
65
|
+
}
|
|
66
|
+
const scalar = hexToBytes(recipe.r);
|
|
67
|
+
if (bytesToNumberBE(scalar) >= CURVE_ORDER) {
|
|
68
|
+
throw new Error("BIP341 NUMS r must be a scalar in 0..n-1");
|
|
69
|
+
}
|
|
70
|
+
return scalar;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reconstruct the public-only extended key used by software and Ledger.
|
|
74
|
+
*
|
|
75
|
+
* Ledger wallet policies currently require an xpub-backed key placeholder and
|
|
76
|
+
* a receive/change derivation suffix. The root point is `H + rG`, where `H` is
|
|
77
|
+
* BIP341's standard NUMS point and `r` is the recipe's public scalar. BIP32
|
|
78
|
+
* public derivation adds only publicly computable point tweaks, so no private
|
|
79
|
+
* key is known unless the discrete logarithm of `H` is known.
|
|
80
|
+
*/
|
|
81
|
+
export function deriveBip341NumsKey(recipe, network) {
|
|
82
|
+
const rBytes = decodeNumsScalar(recipe);
|
|
83
|
+
const h = schnorr.utils.lift_x(bytesToNumberBE(hexToBytes(BIP341_NUMS_H_X)));
|
|
84
|
+
const r = bytesToNumberBE(rBytes);
|
|
85
|
+
const rootPoint = r === 0n ? h : h.add(secp256k1.Point.BASE.multiply(r));
|
|
86
|
+
if (rootPoint.is0()) {
|
|
87
|
+
throw new NonExactBip32DerivationError("BIP341 NUMS root is the point at infinity");
|
|
88
|
+
}
|
|
89
|
+
const rootCompressed = rootPoint.toBytes(true);
|
|
90
|
+
const chainCode = schnorr.utils.taggedHash(NUMS_CHAIN_CODE_TAG, rootCompressed);
|
|
91
|
+
const root = new HDKey({
|
|
92
|
+
versions: HD_VERSIONS[network],
|
|
93
|
+
publicKey: rootCompressed,
|
|
94
|
+
chainCode,
|
|
95
|
+
});
|
|
96
|
+
if (root.privateKey !== null) {
|
|
97
|
+
throw new Error("BIP341 NUMS root must be public-only");
|
|
98
|
+
}
|
|
99
|
+
let branch;
|
|
100
|
+
let child;
|
|
101
|
+
try {
|
|
102
|
+
branch = root.deriveChild(NUMS_BRANCH);
|
|
103
|
+
child = branch.deriveChild(BIP341_NUMS_CHILD_INDEX);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new NonExactBip32DerivationError("BIP32 rejected the requested NUMS derivation");
|
|
107
|
+
}
|
|
108
|
+
if (branch.index !== NUMS_BRANCH || child.index !== BIP341_NUMS_CHILD_INDEX) {
|
|
109
|
+
throw new NonExactBip32DerivationError("BIP32 skipped an invalid NUMS derivation index");
|
|
110
|
+
}
|
|
111
|
+
if (!child.publicKey) {
|
|
112
|
+
throw new Error("Failed to derive BIP341 NUMS internal key");
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
rootCompressed,
|
|
116
|
+
chainCode,
|
|
117
|
+
extendedPublicKey: root.publicExtendedKey,
|
|
118
|
+
internalCompressed: child.publicKey,
|
|
119
|
+
internalXonly: child.publicKey.slice(1),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noBitwiseOperators: Taproot control-block bytes encode parity with bit masks. */
|
|
2
|
+
import type { BTC_NETWORK } from "@scure/btc-signer/utils.js";
|
|
3
|
+
import type { BuildVaultOptions, BuiltVault, User, VaultContract } from "./types.ts";
|
|
4
|
+
export declare function buildAdminRequiredThenKofN(adminXonly: Uint8Array, allXonly: Uint8Array[], t: number): Uint8Array;
|
|
5
|
+
export declare function buildVault(contract: VaultContract, companyUsers: User[], network?: BTC_NETWORK, options?: BuildVaultOptions): BuiltVault;
|