From ca4770ac31a507cc163aa88b42449fc21b63d1c7 Mon Sep 17 00:00:00 2001 From: cmd Date: Sun, 28 Jan 2024 02:14:07 -0600 Subject: [PATCH] update --- src/client/api/contract.ts | 2 +- src/client/api/deposit.ts | 56 ++++++++-------- src/client/api/depositor.ts | 109 ++++++++++++++----------------- src/client/class/signer.ts | 13 ++++ src/client/validators/deposit.ts | 40 ++++++++++++ src/lib/contract.ts | 75 +++++++++++---------- src/lib/deposit.ts | 61 +++++++++++------ src/lib/proposal.ts | 6 +- src/lib/return.ts | 25 +++---- src/lib/session.ts | 30 +++++---- src/lib/tx.ts | 13 ++-- src/lib/util.ts | 3 +- src/schema/deposit.ts | 56 ++++++++-------- src/types/deposit.ts | 68 ++++++++++++------- src/types/signer.ts | 1 + src/validators/deposit.ts | 37 +++-------- test/src/fund.ts | 28 ++++---- test/src/spend.ts | 20 +++--- test/src/tests/e2e.test.ts | 50 +++++++------- test/tape.ts | 2 +- 20 files changed, 385 insertions(+), 310 deletions(-) create mode 100644 src/client/validators/deposit.ts diff --git a/src/client/api/contract.ts b/src/client/api/contract.ts index 0548e952..2e7bf76c 100644 --- a/src/client/api/contract.ts +++ b/src/client/api/contract.ts @@ -74,7 +74,7 @@ function list_contract_api (client : EscrowClient) { token : string ) : Promise> => { // Formulate the request. - const url = `${client.host}/api/contract/list?pubkey=${pubkey}` + const url = `${client.host}/api/contract/list/${pubkey}` // Return the response. return client.fetcher({ url, token }) } diff --git a/src/client/api/deposit.ts b/src/client/api/deposit.ts index 2987a6f9..079c1b3f 100644 --- a/src/client/api/deposit.ts +++ b/src/client/api/deposit.ts @@ -1,16 +1,17 @@ import { EscrowClient } from '../class/client.js' -import { validate_registration } from '@/validators/index.js' +import { validate_register_req } from '@/validators/index.js' import { CovenantData, ReturnData, ApiResponse, - DepositRequest, + AccountRequest, AccountDataResponse, DepositDataResponse, DepositListResponse, - FundingDataResponse + FundingDataResponse, + RegisterRequest } from '@/types/index.js' import * as assert from '@/assert.js' @@ -18,18 +19,20 @@ import * as assert from '@/assert.js' /** * Request a deposit account from the provider. */ -function request_deposit_api (client : EscrowClient) { +function request_account_api (client : EscrowClient) { return async ( - req : DepositRequest + request : AccountRequest ) : Promise> => { - // Ensure params are string values. - const arr = Object.entries(req) - // Build a query string with params. - const qry = new URLSearchParams(arr).toString() // Formulate the request. - const url = `${client.host}/api/deposit/request?${qry}` + const url = `${client.host}/api/deposit/request` + // Formulate the request. + const init = { + method : 'POST', + body : JSON.stringify(request), + headers : { 'content-type' : 'application/json' } + } // Return the response. - return client.fetcher({ url }) + return client.fetcher({ url, init }) } } @@ -38,19 +41,16 @@ function request_deposit_api (client : EscrowClient) { */ function register_deposit_api (client : EscrowClient) { return async ( - agent_id : string, - return_tx : string + request : RegisterRequest ) : Promise> => { - // Create template - const tmpl = { agent_id, return_tx } - // Validate the deposit template. - validate_registration({ agent_id, return_tx }) + // Validate the request. + validate_register_req(request) // Configure the url. const url = `${client.host}/api/deposit/register` // Formulate the request. const init = { method : 'POST', - body : JSON.stringify(tmpl), + body : JSON.stringify(request), headers : { 'content-type' : 'application/json' } } // Return the response. @@ -63,22 +63,18 @@ function register_deposit_api (client : EscrowClient) { */ function register_funds_api (client : EscrowClient) { return async ( - agent_id : string, - return_tx : string, - covenant : CovenantData + request : RegisterRequest ) : Promise> => { // Assert that a covenant is defined. - assert.ok(covenant !== undefined, 'covenant is undefined') - // Create a deposit template. - const templ = { agent_id, return_tx, covenant } - // Validate the deposit template. - validate_registration(templ) + assert.ok(request.covenant !== undefined, 'covenant is undefined') + // Validate the request. + validate_register_req(request) // Formulate the request url. const url = `${client.host}/api/deposit/register` // Forulate the request body. const init = { - method : 'POST', - body : JSON.stringify(templ), + method : 'POST', + body : JSON.stringify(request), headers : { 'content-type' : 'application/json' } } // Return the response. @@ -108,7 +104,7 @@ function list_deposit_api (client : EscrowClient) { token : string ) : Promise> => { // Formulate the request. - const url = `${client.host}/api/deposit/list?pubkey=${pubkey}` + const url = `${client.host}/api/deposit/list/${pubkey}` // Return the response. return client.fetcher({ url, token }) } @@ -153,7 +149,7 @@ export default function (client : EscrowClient) { commit : commit_funds_api(client), fund : register_funds_api(client), register : register_deposit_api(client), - request : request_deposit_api(client), + request : request_account_api(client), close : close_deposit_api(client) } } diff --git a/src/client/api/depositor.ts b/src/client/api/depositor.ts index 6cec6ad4..bdf86642 100644 --- a/src/client/api/depositor.ts +++ b/src/client/api/depositor.ts @@ -1,12 +1,12 @@ import { Buff } from '@cmdcode/buff' -import { create_return_tx } from '@/lib/return.js' +import { parse_extkey } from '@cmdcode/crypto-tools/hd' import { EscrowSigner } from '@/client/class/signer.js' import { get_deposit_ctx } from '@/lib/deposit.js' -import { verify_account } from '@/validators/deposit.js' +import { verify_account } from '@/client/validators/deposit.js' import { create_covenant, - create_return + create_return_psig } from '@/lib/session.js' import { @@ -16,113 +16,102 @@ import { CovenantData, DepositAccount, DepositData, - ReturnData, TxOutput } from '@/types/index.js' -export function request_account_api (client : EscrowSigner) { +export function request_account_api (signer : EscrowSigner) { return async ( - locktime : number + locktime : number, + index ?: number ) : Promise> => { - const pubkey = client.pubkey - return client.client.deposit.request({ pubkey, locktime }) + const deposit_pk = signer.pubkey + const spend_xpub = signer.get_account(index).xpub + const req = { deposit_pk, locktime, spend_xpub } + return signer.client.deposit.request(req) } } export function verify_account_api (signer : EscrowSigner) { return (account : DepositAccount) : void => { - const host_pub = signer.host_pub - const network = signer.client.network - if (host_pub === undefined) { - throw new Error('host pubkey is not set on device') - } - verify_account(account, signer.pubkey, host_pub, network) - } -} - -/** - * Create a deposit template for registration. - */ -export function register_utxo_api (client : EscrowSigner) { - return async ( - account : DepositAccount, - utxo : TxOutput, - txfee ?: number - ) : Promise => { - // Unpack the deposit object. - const { agent_pk, sequence } = account - // Define our pubkey. - const pub = client.pubkey - const idx = Buff.hex(utxo.txid).slice(0, 4).num - const addr = client._wallet.get_account(idx).new_address() - // Get the context object for our deposit account. - const ctx = get_deposit_ctx(agent_pk, pub, sequence) - // Create the return transaction. - return create_return_tx(addr, ctx, client._signer, utxo, txfee) + verify_account(account, signer) } } -export function commit_utxo_api (client : EscrowSigner) { +export function commit_utxo_api (signer : EscrowSigner) { return async ( account : DepositAccount, contract : ContractData, utxo : TxOutput ) : Promise => { // Unpack the deposit object. - const { agent_pk, sequence } = account - // Define our pubkey. - const pub = client.pubkey + const { agent_pk, sequence, spend_xpub } = account + // Check if account xpub is valid. + if (!signer.has_account(spend_xpub)) { + throw new Error('account xpub is not recognized by master wallet') + } + // Define our pubkey as the deposit pubkey. + const deposit_pk = signer.pubkey + // Define our xpub as the return pubkey. + const return_pk = parse_extkey(spend_xpub).pubkey // Get the context object for our deposit account. - const ctx = get_deposit_ctx(agent_pk, pub, sequence) + const ctx = get_deposit_ctx(agent_pk, deposit_pk, return_pk, sequence) // Create a covenant with the contract and deposit. - return create_covenant(ctx, contract, client._signer, utxo) + return create_covenant(ctx, contract, signer._signer, utxo) } } -export function commit_deposit_api (client : EscrowSigner) { +export function commit_deposit_api (signer : EscrowSigner) { return async ( contract : ContractData, deposit : DepositData ) : Promise => { // Unpack the deposit object. - const { agent_pk, sequence, txid, vout, value, scriptkey } = deposit - // Define our pubkey. - const pub = client.pubkey + const { + agent_pk, sequence, txid, vout, + value, scriptkey, spend_xpub + } = deposit + // Check if account xpub is valid. + if (!signer.has_account(spend_xpub)) { + throw new Error('account xpub is not recognized by master wallet') + } + // Define our pubkey as the deposit pubkey. + const deposit_pk = signer.pubkey + // Define our xpub as the return pubkey. + const return_pk = parse_extkey(spend_xpub).pubkey // Get the context object for our deposit account. - const ctx = get_deposit_ctx(agent_pk, pub, sequence) + const ctx = get_deposit_ctx(agent_pk, deposit_pk, return_pk, sequence) // Define utxo object from deposit data. const utxo = { txid, vout, value, scriptkey } // Create a covenant with the contract and deposit. - return create_covenant(ctx, contract, client._signer, utxo) + return create_covenant(ctx, contract, signer._signer, utxo) } } -export function close_deposit_api (client : EscrowSigner) { +export function close_deposit_api (signer : EscrowSigner) { return async ( deposit : DepositData, txfee : number, address ?: string - ) : Promise => { - // Unpack client object. + ) : Promise => { + // Unpack signer object. const { txid } = deposit if (address === undefined) { // Compute an index value from the deposit txid. const acct = Buff.hex(txid).slice(0, 4).num // Generate refund address. - address = client._wallet.get_account(acct).new_address() + address = signer.get_account(acct).new_address() } // Create the return transaction. - return create_return(address, deposit, client._signer, txfee) + return create_return_psig(deposit, signer._signer, txfee) } } -export default function (client : EscrowSigner) { +export default function (signer : EscrowSigner) { return { - request_account : request_account_api(client), - verify_account : verify_account_api(client), - register_utxo : register_utxo_api(client), - commit_utxo : commit_utxo_api(client), - commit_deposit : commit_deposit_api(client), - close_deposit : close_deposit_api(client) + request_account : request_account_api(signer), + verify_account : verify_account_api(signer), + close_account : close_deposit_api(signer), + commit_utxo : commit_utxo_api(signer), + commit_deposit : commit_deposit_api(signer) } } diff --git a/src/client/class/signer.ts b/src/client/class/signer.ts index 6b15832b..47cc9c1a 100644 --- a/src/client/class/signer.ts +++ b/src/client/class/signer.ts @@ -71,6 +71,10 @@ export class EscrowSigner { return this._host_pub } + get network () { + return this._client.network + } + get pubkey () { return this._signer.pubkey } @@ -81,6 +85,15 @@ export class EscrowSigner { request = request_api(this) witness = witness_api(this) + has_account (xpub : string) { + return this._wallet.has_account(xpub) + } + + get_account (idx ?: number) { + idx = idx ?? this._gen_idx() + return this._wallet.get_account(idx) + } + save (password : string) { const pass = Buff.str(password) const encdata = this._signer.backup(pass) diff --git a/src/client/validators/deposit.ts b/src/client/validators/deposit.ts new file mode 100644 index 00000000..3f74e5f8 --- /dev/null +++ b/src/client/validators/deposit.ts @@ -0,0 +1,40 @@ +import { parse_extkey } from '@cmdcode/crypto-tools/hd' +import { verify_sig } from '@cmdcode/crypto-tools/signer' +import { EscrowSigner } from '@/client/index.js' +import { get_object_id } from '@/lib/util.js' + +import { + get_deposit_address, + get_deposit_ctx +} from '@/lib/deposit.js' + +import { DepositAccount } from '@/types/index.js' + +import * as assert from '@/assert.js' + +export function verify_account ( + account : DepositAccount, + signer : EscrowSigner +) { + const { acct_id, acct_sig, ...rest } = account + const { host_pub, network, pubkey } = signer + + const { + address, agent_pk, deposit_pk, + sequence, spend_xpub + } = rest + + assert.ok(host_pub !== undefined, 'host pubkey is not set on device') + assert.ok(pubkey === deposit_pk, 'deposit pubkey does not match device') + assert.ok(signer.has_account(spend_xpub), 'account xpub is not recognized by master wallet') + + const return_pk = parse_extkey(spend_xpub).pubkey + const context = get_deposit_ctx(agent_pk, deposit_pk, return_pk, sequence) + const depo_addr = get_deposit_address(context, network) + const digest = get_object_id(rest) + + assert.ok(address === depo_addr, 'account address does not match context') + assert.ok(digest.hex === acct_id, 'account id does not match digest') + + verify_sig(acct_sig, acct_id, host_pub, { throws : true }) +} diff --git a/src/lib/contract.ts b/src/lib/contract.ts index b8231883..30f5fda7 100644 --- a/src/lib/contract.ts +++ b/src/lib/contract.ts @@ -23,10 +23,8 @@ import { export function create_contract ( config : ContractConfig ) : ContractData { - const agent_fee = config.agent_fee - const feerate = config.feerate - const terms = config.proposal - const outputs = get_spend_outputs(terms, [ agent_fee ]) + const { agent_fee, feerate, proposal: terms } = config + const outputs = get_spend_templates(terms, [ agent_fee ]) const published = config.published ?? now() const signatures = config.signatures ?? [] const subtotal = terms.value + agent_fee[0] @@ -45,7 +43,7 @@ export function create_contract ( expires_at : null, feerate : feerate, moderator : config.moderator ?? null, - outputs : get_spend_outputs(terms, [ agent_fee ]), + outputs : outputs, pending : 0, prop_id : get_proposal_id(terms).hex, pubkeys : signatures.map(e => e.slice(0, 64)), @@ -74,53 +72,60 @@ export function activate_contract ( contract : ContractData, activated : number = now() ) : ContractData { - /** - * Activate a contract. - */ + // Unpack contract object. const { cid, terms } = contract + // Unpack terms object. const { expires, paths, programs, schedule } = terms + // Define a hard expiration date. + const expires_at = activated + expires + // Collect the path names. const pathnames = get_path_names(paths) - return { - ...contract, - activated, - expires_at : activated + expires, - status : 'active', - vm_state : init_vm({ activated, cid, pathnames, programs, schedule }) - } + // Initialize the virtual machine. + const vm_state = init_vm({ activated, cid, pathnames, programs, schedule }) + // Return the activated contract state. + return { ...contract, activated, expires_at, status : 'active', vm_state } } /** - * Returns the effective deadline - * based on the proposal data. + * Returns a relative deadline (in seconds) + * for receiving deposits. */ function get_deadline ( - proposal : ProposalData, - created : number + proposal : ProposalData, + published : number ) { + // Unpack the proposal object. const { deadline, effective } = proposal + // If an effective date is set: if (effective !== undefined) { - return effective - created + // Return remaining time until effective date. + return effective - published } else { - return created + (deadline ?? DEFAULT_DEADLINE) + // Return published date, plus deadline. + return published + (deadline ?? DEFAULT_DEADLINE) } } /** - * Compute the spending output transactions - * for each path in the proposal. + * Convert each spending path in the proposal + * into a transaction output template. */ -export function get_spend_outputs ( - prop : ProposalData, - fees : PaymentEntry[] +export function get_spend_templates ( + proposal : ProposalData, + fees : PaymentEntry[] ) : SpendTemplate[] { - const { payments, paths } = prop - const total_fees = [ ...payments, ...fees ] - const path_names = get_path_names(paths) - const outputs : SpendTemplate[] = [] - for (const name of path_names) { - const vout = get_path_vouts(name, paths, total_fees) + // Unpack proposal object. + const { payments, paths } = proposal + // Collect and sort path names. + const pathnames = get_path_names(paths) + const pay_total = [ ...payments, ...fees ] + // Return labeled array of spend templates. + return pathnames.map(pathname => { + // Get a list of tx outputs. + const vout = get_path_vouts(pathname, paths, pay_total) + // Combine the outputs into a tx template (hex). const txhex = create_txhex(vout) - outputs.push([ name, txhex ]) - } - return outputs + // Return the txhex as an array entry. + return [ pathname, txhex ] + }) } diff --git a/src/lib/deposit.ts b/src/lib/deposit.ts index 323f603c..5706972d 100644 --- a/src/lib/deposit.ts +++ b/src/lib/deposit.ts @@ -1,5 +1,5 @@ -import { parse_deposit } from './parse.js' import { get_return_script } from './return.js' +import { validate_deposit } from '@/validators/deposit.js' import { now, @@ -46,6 +46,7 @@ const GET_INIT_DEPOSIT = () => { ...GET_INIT_SPEND_STATE(), covenant : null, created_at : now(), + return_psig : null, settled : false as const, settled_at : null, spent : false as const, @@ -60,37 +61,51 @@ const GET_INIT_DEPOSIT = () => { export function create_deposit ( template : Partial ) : DepositData { + // Initialize our deposit object. const deposit = { ...GET_INIT_DEPOSIT(), ...template } - + // Initialize the updated date. deposit.updated_at = deposit.created_at - + // If deposit is confirmed: if (deposit.confirmed) { - deposit.status = (deposit.covenant !== null) - ? 'locked' - : 'open' + // If a covenant exists: + if (deposit.covenant !== null) { + // Set the deposit as locked. + deposit.status = 'locked' + } else { + // Set the deposit as open. + deposit.status = 'open' + } } else { + // Set the deposit as pending. deposit.status = 'pending' } - - const parsed = parse_deposit(deposit) - return sort_record(parsed) + // Validate the final object. + validate_deposit(deposit) + // Return a sorted object. + return sort_record(deposit) } /** * Compute a context object for a deposit account. */ export function get_deposit_ctx ( - agent_pk : string, - member_pk : string, - sequence : number + agent_pk : string, + deposit_pk : string, + return_pk : string, + sequence : number ) : DepositContext { - const members = [ member_pk, agent_pk ] - const script = get_return_script(member_pk, sequence) + // Define the members of the multi-sig. + const members = [ agent_pk, deposit_pk ] + // Get the return script path. + const script = get_return_script(return_pk, sequence) + // Get the musig context for the internal key. const int_data = get_key_ctx(members) + // Get the key data for the taproot key. const tap_data = get_tapkey(int_data.group_pubkey.hex, script) + // Get the musig context for the tap-tweaked key. const key_data = tweak_key_ctx(int_data, [ tap_data.taptweak ]) - - return { agent_pk, member_pk, sequence, script, tap_data, key_data } + // Return context object. + return { agent_pk, deposit_pk, return_pk, sequence, script, tap_data, key_data } } /** @@ -100,25 +115,31 @@ export function get_deposit_address ( context : DepositContext, network ?: Network ) { + // Unpack the taproot data from the context. const { tap_data } = context + // Compute the address for the taproot key. return get_address(tap_data.tapkey, network) } /** - * Compute the spend state of a deposit - * using the data received from an oracle. + * Compute the spending state of a deposit, + * using transaction data from an oracle. */ export function get_spend_state ( sequence : number, txstatus : OracleTxStatus ) { + // Initialize our spent state. let state : DepositState = GET_INIT_SPEND_STATE() - + // If transaction is confirmed: if (txstatus !== undefined && txstatus.confirmed) { + // Parse the sequence value back into a timelock. const timelock = parse_timelock(sequence) + // Get the expiration date for the timelock. const expires_at = txstatus.block_time + timelock + // Update the expiration date for the spend state. state = { ...txstatus, expires_at } } - + // Return the spend state. return state } diff --git a/src/lib/proposal.ts b/src/lib/proposal.ts index 69f41cee..d4b1011e 100644 --- a/src/lib/proposal.ts +++ b/src/lib/proposal.ts @@ -57,7 +57,8 @@ export function filter_path ( export function get_path_names ( paths : PathEntry[] ) : string[] { - return [ ...new Set(paths.map(e => e[0])) ] + const pnames = new Set(paths.map(e => e[0])) + return [ ...pnames ].sort() } /** @@ -77,7 +78,8 @@ export function get_pay_total ( export function get_addrs ( paths : PathEntry[] ) : string[] { - return [ ...new Set(paths.map(e => e[2])) ] + const addrs = new Set(paths.map(e => e[2])) + return [ ...addrs ] } /** diff --git a/src/lib/return.ts b/src/lib/return.ts index 8c329f26..3cbb2980 100644 --- a/src/lib/return.ts +++ b/src/lib/return.ts @@ -76,26 +76,27 @@ export function get_return_script ( * for a given unspent transaction output. */ export function create_return_tx ( - address : string, - context : DepositContext, - signer : SignerAPI, - txout : TxOutput, + address : string, + context : DepositContext, + signer : SignerAPI, + txout : TxOutput, txfee = MIN_RECOVER_FEE ) : string { - const { sequence, tap_data } = context - const { cblock, extension, script } = tap_data - assert.ok(txout.value > txfee, 'tx value does not cover txfee') + const { return_pk, sequence, tap_data } = context + const { cblock, extension, script } = tap_data + assert.ok(txout.value > txfee, 'tx value does not cover txfee') + assert.ok(signer.pubkey === return_pk, 'signer does not control return pubkey') assert.exists(script) - const scriptkey = parse_addr(address).asm - const txin = create_txinput(txout) + const tx_input = create_txinput(txout) const return_tx = create_tx({ - vin : [{ ...txin, sequence }], + vin : [{ ...tx_input, sequence }], vout : [{ value : txout.value - txfee, - scriptPubKey : scriptkey + scriptPubKey : parse_addr(address).asm }] }) - const opt : SigHashOptions = { extension, pubkey: signer.pubkey, txindex : 0, throws: true } + + const opt : SigHashOptions = { extension, txindex : 0, throws: true } const sig = sign_tx(signer, return_tx, opt) return_tx.vin[0].witness = [ sig, script, cblock ] // assert.ok(taproot.verify_tx(recover_tx, opt), 'recovery tx failed to generate!') diff --git a/src/lib/session.ts b/src/lib/session.ts index 0f73dccc..6a826433 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,5 +1,6 @@ import { Buff, Bytes } from '@cmdcode/buff' import { hash340, sha512 } from '@cmdcode/crypto-tools/hash' +import { parse_extkey } from '@cmdcode/crypto-tools/hd' import { tweak_pubkey } from '@cmdcode/crypto-tools/keys' import { TxPrevout } from '@scrow/tapscript' import { get_deposit_ctx } from './deposit.js' @@ -12,7 +13,7 @@ import { import { create_sighash, - create_tx_tmpl, + create_tx_template, create_txinput, parse_txinput } from './tx.js' @@ -30,7 +31,6 @@ import { DepositData, MutexContext, MutexEntry, - ReturnData, SignerAPI, TxOutput } from '../types/index.js' @@ -81,26 +81,29 @@ export function create_covenant ( * signature, to be used for collaboratively * returning a deposit back to the sender. */ -export function create_return ( - address : string, +export function create_return_psig ( deposit : DepositData, signer : SignerAPI, txfee : number -) : ReturnData { +) : string { // Unpack the deposit object. - const { agent_id, dpid, agent_pn, value } = deposit + const { agent_id, agent_pn, dpid, value, spend_xpub } = deposit + // Parse the return pubkey from the xpub. + const return_pk = parse_extkey(spend_xpub).pubkey // Compute the session pnonce value. const pnonce = get_session_pnonce(agent_id, dpid, signer).hex // Combine pnonces into a list. const pnonces = [ pnonce, agent_pn ] + // Create locking script. + const script = [ 'OP_1', return_pk ] // Create a return transaction using the provided params. - const txhex = create_tx_tmpl(address, value - txfee) + const txhex = create_tx_template(script, value - txfee) // Compute a musig context object for the transaction. const mutex = get_return_mutex(deposit, pnonces, txhex) // Create a partial signature using the musig context. const psig = create_mutex_psig(mutex, signer) - // Return the final payload. - return { dpid, pnonce, psig, txhex } + // Return the pnonce and psig. + return Buff.join([ pnonce, psig ]).hex } /** @@ -138,9 +141,14 @@ export function get_return_mutex ( txhex : string ) : MutexContext { // Unpack the deposit object. - const { agent_id, agent_pk, dpid, member_pk, sequence } = deposit + const { + agent_id, agent_pk, dpid, + deposit_pk, sequence, spend_xpub + } = deposit + // Parse the return pubkey from the xpub. + const return_pk = parse_extkey(spend_xpub).pubkey // Get a context object for the deposit. - const dep_ctx = get_deposit_ctx(agent_pk, member_pk, sequence) + const dep_ctx = get_deposit_ctx(agent_pk, deposit_pk, return_pk, sequence) // Compute the session id for the agent and deposit. const sid = get_session_id(agent_id, dpid) // Parse the txinput from the deposit data. diff --git a/src/lib/tx.ts b/src/lib/tx.ts index b5f266de..b029a4e8 100644 --- a/src/lib/tx.ts +++ b/src/lib/tx.ts @@ -1,4 +1,5 @@ import { Buff, Bytes } from '@cmdcode/buff' +import { P2TR } from '@scrow/tapscript/address' import { taproot } from '@scrow/tapscript/sighash' import { parse_script } from '@scrow/tapscript/script' import { tap_pubkey } from '@scrow/tapscript/tapkey' @@ -18,11 +19,6 @@ import { TxPrevout } from '@scrow/tapscript' -import { - P2TR, - parse_addr -} from '@scrow/tapscript/address' - import { create_prevout, parse_sequence, @@ -161,11 +157,10 @@ export function get_signed_tx ( return encode_tx(txdata) } -export function create_tx_tmpl ( - address : string, - value : number +export function create_tx_template ( + script : string[], + value : number ) { - const script = parse_addr(address).asm const txout = create_vout({ value, scriptPubKey : script }) return create_txhex([ txout ]) } diff --git a/src/lib/util.ts b/src/lib/util.ts index d5a3edfc..4bb834f4 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -160,7 +160,8 @@ export function stringify (content : any) : string { } } -export function get_object_id (obj : T) : Buff { +export function get_object_id ( + obj : T) : Buff { if (Array.isArray(obj) || typeof obj !== 'object') { throw new Error('not an object') } diff --git a/src/schema/deposit.ts b/src/schema/deposit.ts index fe4abcfd..f6445c47 100644 --- a/src/schema/deposit.ts +++ b/src/schema/deposit.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import base from './base.js' import tx from './tx.js' - + const { bech32, hash, hex, nonce, num, psig, stamp, str } = base const { close_state, spend_state, txspend } = tx @@ -25,20 +25,16 @@ const locktime = z.union([ str, num ]).transform(e => Number(e)) const state = z.discriminatedUnion('confirmed', [ confirmed, unconfirmed ]) const status = z.enum([ 'reserved', 'pending', 'stale', 'open', 'locked', 'spent', 'settled', 'expired', 'error' ]) -const request = z.object({ - locktime : locktime.optional(), - pubkey : hash -}) - const account = z.object({ + acct_id : hash, + acct_sig : nonce, address : bech32, agent_id : hash, agent_pk : hash, created_at : stamp, - dpid : hash, - member_pk : hash, + deposit_pk : hash, sequence : num, - sig : nonce + spend_xpub : str }) const covenant = z.object({ @@ -47,31 +43,33 @@ const covenant = z.object({ psigs : z.tuple([ str, psig ]).array() }) -const register = z.object({ - agent_id : hash, - covenant : covenant.optional(), - return_tx : hex, +const account_req = z.object({ + deposit_pk : hash, + locktime : locktime.optional(), + spend_xpub : str }) -const refund = z.object({ - dpid : hash, - pnonce : nonce, - psig, - txhex : hex +const register_req = z.object({ + covenant : covenant.optional(), + deposit_pk : hash, + return_psig : hex.optional(), + sequence : num, + spend_xpub : str }) const data = z.object({ status, - agent_id : hash, - agent_pk : hash, - agent_pn : nonce, - covenant : covenant.nullable(), - created_at : stamp, - dpid : hash, - member_pk : hash, - return_tx : hex, - sequence : num, - updated_at : stamp + agent_id : hash, + agent_pk : hash, + agent_pn : nonce, + covenant : covenant.nullable(), + created_at : stamp, + dpid : hash, + deposit_pk : hash, + return_psig : hex.nullable(), + sequence : num, + spend_xpub : str, + updated_at : stamp }).and(state).and(spend_state).and(close_state).and(txspend) -export default { account, covenant, data, state, refund, request, register, status } +export default { account, covenant, data, state, account_req, register_req, status } diff --git a/src/types/deposit.ts b/src/types/deposit.ts index 101595cc..e2b04c37 100644 --- a/src/types/deposit.ts +++ b/src/types/deposit.ts @@ -38,12 +38,13 @@ interface DepositUnconfirmed { } export interface DepositContext { - agent_pk : string - member_pk : string - key_data : KeyContext - script : ScriptWord[] - sequence : number - tap_data : TapContext + agent_pk : string + deposit_pk : string + key_data : KeyContext + return_pk : string + script : ScriptWord[] + sequence : number + tap_data : TapContext } export interface ReturnContext { @@ -55,25 +56,27 @@ export interface ReturnContext { } export interface DepositAccount { - created_at : number + acct_id : string + acct_sig : string address : string agent_id : string agent_pk : string - member_pk : string - req_id : string + created_at : number + deposit_pk : string sequence : number - sig : string + spend_xpub : string } export interface DepositInfo { - covenant : CovenantData | null - created_at : number - dpid : string - member_pk : string - return_tx : string - sequence : number - status : DepositStatus - updated_at : number + covenant : CovenantData | null + created_at : number + deposit_pk : string + dpid : string + return_psig : string | null + sequence : number + status : DepositStatus + updated_at : number + spend_xpub : string } export interface ReturnData { @@ -83,13 +86,28 @@ export interface ReturnData { txhex : string } -export interface DepositRequest { - pubkey : string - locktime : number +export interface AccountRequest { + deposit_pk : string + locktime ?: number + spend_xpub : string +} + +export interface RegisterRequest { + covenant ?: CovenantData + deposit_pk : string + return_psig ?: string + sequence : number + spend_xpub : string } -export interface DepositRegister { - agent_id : string - covenant ?: CovenantData - return_tx : string +export interface ExtendedKey { + prefix : number + depth : number + fprint : number + index : number + code : string + type : number + key : string + seckey : string + pubkey : string } diff --git a/src/types/signer.ts b/src/types/signer.ts index e85a4e49..fb7ac2ad 100644 --- a/src/types/signer.ts +++ b/src/types/signer.ts @@ -40,6 +40,7 @@ export interface SignOptions { export interface WalletAPI { xpub : string + has_account : (extkey : string) => boolean get_account : (id : Bytes) => WalletAPI has_address : (addr : string, limit ?: number) => boolean new_address : () => string diff --git a/src/validators/deposit.ts b/src/validators/deposit.ts index bff1ca51..1f192432 100644 --- a/src/validators/deposit.ts +++ b/src/validators/deposit.ts @@ -1,19 +1,10 @@ -import { Bytes } from '@cmdcode/buff' import { taproot } from '@scrow/tapscript/sighash' import { parse_sequence } from '@scrow/tapscript/tx' -import { verify_sig } from '@cmdcode/crypto-tools/signer' import { - get_deposit_address, - get_deposit_ctx -} from '@/lib/deposit.js' - -import { - DepositAccount, DepositContext, DepositData, - DepositRegister, - Network, + RegisterRequest, ReturnContext, TxOutput } from '../types/index.js' @@ -21,10 +12,16 @@ import { import * as assert from '../assert.js' import * as schema from '../schema/index.js' -export function validate_registration ( +export function validate_account_req ( + template : unknown +) : asserts template is RegisterRequest { + schema.deposit.account_req.parse(template) +} + +export function validate_register_req ( template : unknown -) : asserts template is DepositRegister { - schema.deposit.register.parse(template) +) : asserts template is RegisterRequest { + schema.deposit.register_req.parse(template) } export function validate_deposit ( @@ -33,20 +30,6 @@ export function validate_deposit ( schema.deposit.data.parse(deposit) } -export function verify_account ( - account : DepositAccount, - fund_pub : Bytes, - host_pub : Bytes, - network : Network -) { - const { address, agent_pk, member_pk, req_id, sequence, sig } = account - const ctx = get_deposit_ctx(agent_pk, member_pk, sequence) - const addr = get_deposit_address(ctx, network) - assert.ok(fund_pub === member_pk, 'member pubkey does not match!') - assert.ok(address === addr, 'account address does not match!') - verify_sig(sig, req_id, host_pub, { throws : true }) -} - export function verify_deposit ( deposit_ctx : DepositContext, return_ctx : ReturnContext, diff --git a/test/src/fund.ts b/test/src/fund.ts index e1016d85..2632d2cf 100644 --- a/test/src/fund.ts +++ b/test/src/fund.ts @@ -1,5 +1,4 @@ import { create_covenant } from '@scrow/core/session' -import { create_return_tx } from '@scrow/core/return' import { create_timelock } from '@scrow/core/tx' import { get_utxo } from './core.js' import { CoreSigner } from './types.js' @@ -14,30 +13,33 @@ import { get_deposit_ctx } from '@scrow/core/deposit' -const SEQUENCE = create_timelock(60 * 60 * 2) +const locktime = 60 * 60 * 2 -export function get_funds ( +export async function register_funds ( contract : ContractData, members : CoreSigner[], txfee = 1000 ) { - const { agent_id, agent_pk } = contract + const { agent_pk } = contract const cli = members[0].core.client const faucet = cli.core.faucet const network = contract.terms.network const value = Math.ceil(contract.total / 3 + txfee) - const templates = members.map(async mbr => { - const ctx = get_deposit_ctx(agent_pk, mbr.signer.pubkey, SEQUENCE) + const templates = members.map(async (mbr) => { + const deposit_pk = mbr.signer.pubkey + const return_pk = mbr.wallet.pubkey + const sequence = create_timelock(locktime) + const spend_xpub = mbr.wallet.xpub + const ctx = get_deposit_ctx(agent_pk, deposit_pk, return_pk, sequence) const addr = get_deposit_address(ctx, network) + console.log('addr1:', addr) await faucet.ensure_funds(value) const txid = await faucet.send_funds(value, addr) - const txo = await get_utxo(cli, addr, txid) - assert.exists(txo) - const ret = await mbr.core.new_address - const rtx = create_return_tx(ret, ctx, mbr.signer, txo, txfee) - const cov = create_covenant(ctx, contract, mbr.signer, txo) - return { agent_id, covenant : cov, return_tx : rtx } + const utxo = await get_utxo(cli, addr, txid) + assert.exists(utxo) + const covenant = create_covenant(ctx, contract, mbr.signer, utxo) + return { covenant, deposit_pk, sequence, spend_xpub, utxo } }) - + await cli.mine_blocks(1) return Promise.all(templates) } diff --git a/test/src/spend.ts b/test/src/spend.ts index 9e7b5293..10bc37a3 100644 --- a/test/src/spend.ts +++ b/test/src/spend.ts @@ -1,6 +1,7 @@ import { combine_psigs } from '@cmdcode/musig2' import { Signer } from '@cmdcode/signer' +import { parse_extkey } from '@cmdcode/crypto-tools/hd' import { decode_tx } from '@scrow/tapscript/tx' import { get_deposit_ctx } from '../../src/lib/deposit.js' import { parse_txinput } from '../../src/lib/tx.js' @@ -70,17 +71,18 @@ export function sign_covenant ( output : SpendTemplate, txinput : TxPrevout ) : string { - const { covenant, member_pk, sequence } = deposit - const { agent_id, cid, agent_pn } = contract + const { covenant, deposit_pk, sequence, spend_xpub } = deposit + const { agent_id, cid, agent_pn } = contract assert.exists(covenant) const [ label, vout ] = output const { pnonce, psigs } = covenant - const dep_ctx = get_deposit_ctx(agent.pubkey, member_pk, sequence) - const pnonces = [ pnonce, agent_pn ] - const sid = get_session_id(agent_id, cid) - const mut_ctx = get_mutex_ctx(dep_ctx, vout, pnonces, sid, txinput) - const psig_a = create_mutex_psig(mut_ctx, agent) - const psig_d = get_entry(label, psigs) - const musig = combine_psigs(mut_ctx.mutex, [ psig_d, psig_a ]) + const return_pk = parse_extkey(spend_xpub).pubkey + const dep_ctx = get_deposit_ctx(agent.pubkey, deposit_pk, return_pk, sequence) + const pnonces = [ pnonce, agent_pn ] + const sid = get_session_id(agent_id, cid) + const mut_ctx = get_mutex_ctx(dep_ctx, vout, pnonces, sid, txinput) + const psig_a = create_mutex_psig(mut_ctx, agent) + const psig_d = get_entry(label, psigs) + const musig = combine_psigs(mut_ctx.mutex, [ psig_d, psig_a ]) return musig.append(0x81).hex } diff --git a/test/src/tests/e2e.test.ts b/test/src/tests/e2e.test.ts index 43b40f35..55b5e3a9 100644 --- a/test/src/tests/e2e.test.ts +++ b/test/src/tests/e2e.test.ts @@ -1,13 +1,13 @@ import { Test } from 'tape' import { Buff } from '@cmdcode/buff' import { CoreClient } from '@cmdcode/core-cmd' +import { parse_extkey } from '@cmdcode/crypto-tools/hd' import { Signer } from '@cmdcode/signer' -import { get_return_ctx } from '@scrow/core/return' +import { prevout_to_txspend } from '@scrow/core/tx' import { create_session } from '@scrow/core/session' import { now } from '@scrow/core/util' -import { prevout_to_txspend } from '@scrow/core/tx' import { get_members } from '../core.js' -import { get_funds } from '../fund.js' +import { register_funds } from '../fund.js' import { create_settlment } from '../spend.js' import { @@ -32,20 +32,19 @@ import { } from '@scrow/core/vm' import { - verify_deposit, validate_proposal, verify_proposal, - validate_covenant, verify_covenant, - validate_registration, + validate_register_req, verify_witness, validate_witness } from '@scrow/core/validate' +import { PaymentEntry } from '@/types/index.js' + import * as assert from '@scrow/core/assert' import { get_proposal } from '../vectors/basic_escrow.js' -import { PaymentEntry } from '@/index.js' const VERBOSE = process.env.VERBOSE === 'true' @@ -90,27 +89,28 @@ export default async function (client : CoreClient, tape : Test) { /* ------------------- [ Funding ] ------------------- */ - const templates = await get_funds(contract, members) - - const promises = templates.map(async tmpl => { - validate_registration(tmpl) - validate_covenant(tmpl.covenant) - const return_ctx = get_return_ctx(tmpl.return_tx) - const { pubkey, sequence } = return_ctx - const { txid, vout } = return_ctx.tx.vin[0] - const deposit_key = agent.signer.pubkey - const deposit_ctx = get_deposit_ctx(deposit_key, pubkey, sequence) - const data = await client.get_txinput(txid, vout) - assert.exists(data) - const spendout = prevout_to_txspend(data.txinput) - verify_deposit(deposit_ctx, return_ctx, spendout) + const registrations = await register_funds(contract, members) + + const promises = registrations.map(async tmpl => { + const { utxo, ...template } = tmpl + const { deposit_pk, sequence, spend_xpub } = template + const { txid, vout } = utxo + validate_register_req(template) + const agent_id = agent.signer.id + const agent_pk = agent.signer.pubkey + const return_pk = parse_extkey(spend_xpub).pubkey + const dep_ctx = get_deposit_ctx(agent_pk, deposit_pk, return_pk, sequence) + const tx_data = await client.get_txinput(txid, vout) + assert.ok(tx_data !== null, 'there is no tx data') + const spendout = prevout_to_txspend(tx_data.txinput) + // verify_deposit(dep_ctx, return_ctx, spendout) const dpid = Buff.random(32).hex - const state = get_spend_state(sequence, data.status) + const state = get_spend_state(sequence, tx_data.status) const session = create_session(agent.signer, dpid) const agent_pn = session.agent_pn - const deposit = create_deposit({ ...deposit_ctx, dpid, agent_pn, ...tmpl, ...spendout, ...state }) - // const deposit = register_deposit(deposit_ctx, dep_id, pnonce, tmpl, spendout, state) - verify_covenant(deposit_ctx, contract, deposit, agent.signer, agent.signer) + const deposit = create_deposit({ ...dep_ctx, dpid, agent_id, agent_pn, ...template, ...spendout, ...state }) + // const deposit = register_deposit(dep_ctx, dpid, pnonce, tmpl, spendout, state) + verify_covenant(dep_ctx, contract, deposit, agent.signer, agent.signer) return deposit }) diff --git a/test/tape.ts b/test/tape.ts index 71c47109..c45f3b5c 100644 --- a/test/tape.ts +++ b/test/tape.ts @@ -5,7 +5,7 @@ import e2e_test from './src/tests/e2e.test.js' import vm_test from './src/vm/vm.test.js' tape('Escrow Core Test Suite', async t => { - vm_test(t) + // vm_test(t) const core = get_daemon() const client = await core.startup() await e2e_test(client, t)