REF: BIP47 under the hood

This commit is contained in:
overtorment 2024-04-29 23:43:04 +01:00 committed by Overtorment
parent e296f2b80b
commit e1f18f3a29
5 changed files with 290 additions and 17 deletions

View file

@ -333,7 +333,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// next, bip47 addresses
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
let hasUnconfirmed = false;
this._txs_by_payment_code_index[pc] = this._txs_by_payment_code_index[pc] || {};
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c] || [];
@ -394,7 +394,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
}
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
this._txs_by_payment_code_index[pc][c] = this._txs_by_payment_code_index[pc][c].filter(tx => !!tx.confirmations);
}
}
@ -485,7 +485,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
for (const tx of Object.values(txdatas)) {
for (const vin of tx.vin) {
if (vin.addresses && vin.addresses.indexOf(this._getBIP47AddressReceive(pc, c)) !== -1) {
@ -563,7 +563,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
if (this._receive_payment_codes)
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + 1; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + 1; c++) {
ownedAddressesHashmap[this._getBIP47AddressReceive(pc, c)] = true;
}
}
@ -881,7 +881,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
for (const pc of this._receive_payment_codes) {
let confirmed = 0;
let unconfirmed = 0;
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
const addr = this._getBIP47AddressReceive(pc, c);
if (balances.addresses[addr].confirmed || balances.addresses[addr].unconfirmed) {
confirmed = confirmed + balances.addresses[addr].confirmed;
@ -1003,7 +1003,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
ownedAddressesHashmap[this._getInternalAddressByIndex(c)] = true;
}
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + 1; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + 1; c++) {
ownedAddressesHashmap[this._getBIP47AddressReceive(pc, c)] = true;
}
}
@ -1061,7 +1061,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (this._getInternalAddressByIndex(c) === address) return path + '/1/' + c;
}
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
// not technically correct but well, to have at least somethign in PSBT...
if (this._getBIP47AddressReceive(pc, c) === address) return "m/47'/0'/0'/" + c;
}
@ -1083,7 +1083,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (this._getInternalAddressByIndex(c) === address) return this._getNodePubkeyByIndex(1, c);
}
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
if (this._getBIP47AddressReceive(pc, c) === address) return this._getBIP47PubkeyByIndex(pc, c);
}
}
@ -1105,7 +1105,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (this._getInternalAddressByIndex(c) === address) return this._getWIFByIndex(true, c);
}
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
if (this._getBIP47AddressReceive(pc, c) === address) return this._getBIP47WIF(pc, c);
}
}
@ -1127,7 +1127,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (this._getInternalAddressByIndex(c) === cleanAddress) return true;
}
for (const pc of this._receive_payment_codes) {
for (let c = 0; c < this._getNextFreePaymentCodeAddress(pc) + this.gap_limit; c++) {
for (let c = 0; c < this._getNextFreePaymentCodeIndexReceive(pc) + this.gap_limit; c++) {
if (this._getBIP47AddressReceive(pc, c) === address) return true;
}
}
@ -1150,9 +1150,9 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
targets: CreateTransactionTarget[],
feeRate: number,
changeAddress: string,
sequence: number,
sequence: number = AbstractHDElectrumWallet.defaultRBFSequence,
skipSigning = false,
masterFingerprint: number,
masterFingerprint: number = 0,
): CreateTransactionResult {
if (targets.length === 0) throw new Error('No destination provided');
// compensating for coinselect inability to deal with segwit inputs, and overriding script length for proper vbytes calculation
@ -1240,6 +1240,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// this is not correct fingerprint, as we dont know realfingerprint - we got zpub with 84/0, but fingerpting
// should be from root. basically, fingerprint should be provided from outside by user when importing zpub
if (output.address?.startsWith('PM')) {
// ok its BIP47 payment code, so we need to unwrap a joint address for the receiver and use it instead:
output.address = this._getNextFreePaymentCodeAddressSend(output.address);
}
psbt.addOutput({
address: output.address,
// @ts-ignore types from bitcoinjs are not exported so we cant define outputData separately and add fields conditionally (either address or script should be present)
@ -1532,6 +1537,60 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return this._bip47_instance;
}
/**
* this method goes over all our txs and checks if we sent a notification tx in the past to the given PC
*/
needToNotifyBIP47(receiverPaymentCode: string): boolean {
const publicBip47 = BIP47Factory(ecc).fromPaymentCode(receiverPaymentCode);
const remoteNotificationAddress = publicBip47.getNotificationAddress();
for (const tx of this.getTransactions()) {
for (const output of tx.outputs) {
if (output.scriptPubKey?.addresses?.includes(remoteNotificationAddress)) return false;
// ^^^ if in the past we sent a tx to his notification address - most likely that was a proper notification
// transaction with OP_RETURN.
// but not gona verify it here, will just trust it
}
}
return true;
}
/**
* return BIP47 payment code of the counterparty of this transaction (someone paid us, or we paid someone)
* or false if it was a non-BIP47 transaction
*/
getSenderByTxid(txid: string): string | false {
for (const pc of Object.keys(this._txs_by_payment_code_index)) {
// iterating all payment codes
for (const txs of Object.values(this._txs_by_payment_code_index[pc])) {
for (const tx of txs) {
if (tx.txid === txid) {
return pc; // found it!
}
}
}
}
// checking txs we sent to counterparties
for (const pc of this._send_payment_codes) {
for (const tx of this.getTransactions().filter(transaction => transaction.txid === txid)) {
for (const out of tx.outputs) {
for (const address of out.scriptPubKey?.addresses ?? []) {
if (this._addresses_by_payment_code_send[pc] && Object.values(this._addresses_by_payment_code_send[pc]).includes(address)) {
// found it!
return pc;
}
}
}
}
}
return false; // found nothing
}
createBip47NotificationTransaction(utxos: CreateTransactionUtxo[], receiverPaymentCode: string, feeRate: number, changeAddress: string) {
const aliceBip47 = BIP47Factory(ecc).fromBip39Seed(this.getSecret(), undefined, this.getPassphrase());
const bobBip47 = BIP47Factory(ecc).fromPaymentCode(receiverPaymentCode);
@ -1652,7 +1711,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
/**
* for counterparties we can pay, we sync shared addresses to find the one we havent used yet.
* this method could benefit from rewriting in batch requests, but not necessary - its only going to be called
* once in a while (when user decides to pay agiven counterparty again)
* once in a while (when user decides to pay a given counterparty again)
*/
async syncBip47ReceiversAddresses(receiverPaymentCode: string) {
this._next_free_payment_code_address_index_send[receiverPaymentCode] =
@ -1670,6 +1729,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
// empty address, stop here, we found our latest index and filled array with shared addresses
this._next_free_payment_code_address_index_send[receiverPaymentCode] = c;
break;
}
}
@ -1678,6 +1738,18 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return this._receive_payment_codes;
}
getBIP47ReceiverPaymentCodes(): string[] {
return this._send_payment_codes;
}
/**
* adding counterparty whom we can pay. trusting that notificaton transaction is in place already
*/
addBIP47Receiver(paymentCode: string) {
if (this._send_payment_codes.includes(paymentCode)) return; // duplicates
this._send_payment_codes.push(paymentCode);
}
_hdNodeToAddress(hdNode: BIP32Interface): string {
return this._nodeToBech32SegwitAddress(hdNode);
}
@ -1709,6 +1781,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
if (!this._addresses_by_payment_code_send[paymentCode]) this._addresses_by_payment_code_send[paymentCode] = [];
if (this._addresses_by_payment_code_send[paymentCode][index]) {
// cache hit
return this._addresses_by_payment_code_send[paymentCode][index];
}
@ -1718,10 +1791,19 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return address;
}
_getNextFreePaymentCodeAddress(paymentCode: string) {
_getNextFreePaymentCodeIndexReceive(paymentCode: string) {
return this._next_free_payment_code_address_index_receive[paymentCode] || 0;
}
/**
* when sending funds to a payee, this method will return next unused joint address for him.
* this method assumes that we synced our payee via `syncBip47ReceiversAddresses()`
*/
_getNextFreePaymentCodeAddressSend(paymentCode: string) {
this._next_free_payment_code_address_index_send[paymentCode] = this._next_free_payment_code_address_index_send[paymentCode] || 0;
return this._getBIP47AddressSend(paymentCode, this._next_free_payment_code_address_index_send[paymentCode]);
}
_getBalancesByPaymentCodeIndex(paymentCode: string): BalanceByIndex {
return this._balances_by_payment_code_index[paymentCode] || { c: 0, u: 0 };
}

View file

@ -615,6 +615,7 @@
"payment_code": "Payment Code",
"payment_codes_list": "Payment Codes List",
"who_can_pay_me": "Who can pay me:",
"whom_can_i_pay": "Whom can I pay:",
"purpose": "Reusable and shareable code (BIP47)",
"not_found": "Payment code not found"
}

View file

@ -5,6 +5,7 @@ import { PaymentCodeStackParamList } from '../../Navigation';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import loc from '../../loc';
import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import { AbstractHDElectrumWallet } from '../../class/wallets/abstract-hd-electrum-wallet';
interface DataSection {
title: string;
@ -21,15 +22,18 @@ export default function PaymentCodesList({ route }: Props) {
useEffect(() => {
if (!walletID) return;
const foundWallet = wallets.find(w => w.getID() === walletID);
const foundWallet = wallets.find(w => w.getID() === walletID) as unknown as AbstractHDElectrumWallet;
if (!foundWallet) return;
const newData: DataSection[] = [
{
title: loc.bip47.who_can_pay_me,
// @ts-ignore remove later
data: foundWallet.getBIP47SenderPaymentCodes(),
},
{
title: loc.bip47.whom_can_i_pay,
data: foundWallet.getBIP47ReceiverPaymentCodes(),
},
];
setData(newData);
}, [walletID, wallets]);

View file

@ -143,4 +143,105 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
bobBip47.getNotificationAddress(),
); // transaction is to Bob's notification address
});
it('can tell whom to notify and whom dont', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
// whom we are going to notify:
const bip47instanceReceiver = BIP47Factory(ecc).fromBip39Seed(process.env.BIP47_HD_MNEMONIC.split(':')[0], undefined, '1');
// notifier:
const walletSender = new HDSegwitBech32Wallet();
walletSender.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
walletSender.switchBIP47(true);
await walletSender.fetchBIP47SenderPaymentCodes();
await walletSender.fetchBalance();
await walletSender.fetchTransactions();
assert.ok(walletSender.getTransactions().length >= 3);
assert.ok(walletSender._receive_payment_codes.length === 1);
assert.strictEqual(walletSender.needToNotifyBIP47(bip47instanceReceiver.getSerializedPaymentCode()), false); // already notified in the past
assert.strictEqual(
walletSender.needToNotifyBIP47(
'PM8TJdfXvRasx4WNpxky25ZKxhvfEiGYW9mka92tfiqDRSL7LQdxnC8uAk9k3okXctZowVwY2PUndjCQR6DHyuVVwqmy2aodmZNHgfFZcJRNTuBAXJCp',
),
true,
); // random PC from interwebz. never interacted with him, so need to notify
});
it('can tell with which counterparty PC transaction is', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
const w = new HDSegwitBech32Wallet();
w.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[0]);
w.setPassphrase('1');
w.switchBIP47(true);
await w.fetchBIP47SenderPaymentCodes();
await w.fetchBalance();
await w.fetchTransactions();
assert.ok(
w
.getBIP47SenderPaymentCodes()
.includes('PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo'),
); // sparrow payment code
assert.strictEqual(
w.getTransactions().find(tx => tx.txid === '64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f')?.value,
100000,
); // sparrow paid us after sparrow made a notification tx
assert.ok(
!w.needToNotifyBIP47(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
),
);
assert.strictEqual(
w.getSenderByTxid('64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f'),
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
); // we got paid
// pretending that user added this PC as a counterparty (sent a notif tx) to pay to:
w.addBIP47Receiver(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
);
assert.ok(
!w.needToNotifyBIP47(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
),
); // dont need to notify
// prior to sync, we have no info on which joint address shall be available
assert.strictEqual(
w._next_free_payment_code_address_index_send
.PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo,
undefined, // basically zero
);
await w.syncBip47ReceiversAddresses(
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
);
// after sync, we know that index 0 was used so index 1 is next free:
assert.strictEqual(
w._next_free_payment_code_address_index_send
.PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo,
1,
);
assert.strictEqual(
w.getSenderByTxid('73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d'),
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
); // we paid sparrow
});
});

View file

@ -157,7 +157,7 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
const changeAddress = 'bc1q7vraw79vcf7qhnefeaul578h7vjc7tr95ywfuq';
const { tx, fee } = await walletSender.createBip47NotificationTransaction(
const { tx, fee } = walletSender.createBip47NotificationTransaction(
utxos,
bip47instanceReceiver.getSerializedPaymentCode(),
33,
@ -177,6 +177,91 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
assert.strictEqual(Math.round(actualFeerate), 33);
});
it('should be able to pay to PC', async () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');
return;
}
// whom we are going to pay:
const bip47instanceReceiver = BIP47Factory(ecc).fromBip39Seed(process.env.BIP47_HD_MNEMONIC.split(':')[0], undefined, '1');
// notifier:
const walletSender = new HDSegwitBech32Wallet();
walletSender.setSecret(process.env.BIP47_HD_MNEMONIC.split(':')[1]);
walletSender.switchBIP47(true);
// since we cant do network calls, we hardcode our senders so later `_getWIFbyAddress`
// could resolve wif for address deposited by him (funds we want to use reside on addresses from BIP47)
walletSender._receive_payment_codes = [
'PM8TJXuZNUtSibuXKFM6bhCxpNaSye6r4px2GXRV5v86uRdH9Raa8ZtXEkG7S4zLREf4ierjMsxLXSFTbRVUnRmvjw9qnc7zZbyXyBstSmjcb7uVcDYF',
];
walletSender.addBIP47Receiver(bip47instanceReceiver.getSerializedPaymentCode());
const utxos: CreateTransactionUtxo[] = [
{
value: 74822,
address: 'bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt',
txid: '73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d',
vout: 0,
wif: walletSender._getWIFbyAddress('bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt') + '',
},
{
value: 894626,
address: 'bc1qr60ek5gtjs04akcp9f5x25v5gyp2tmspx78jxl',
txid: '64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f',
vout: 0,
wif: walletSender._getWIFbyAddress('bc1qr60ek5gtjs04akcp9f5x25v5gyp2tmspx78jxl') + '',
},
];
const changeAddress = 'bc1q7vraw79vcf7qhnefeaul578h7vjc7tr95ywfuq';
const { tx, fee } = walletSender.createTransaction(
utxos,
[
{ address: bip47instanceReceiver.getSerializedPaymentCode(), value: 10234 },
{ address: '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS', value: 22000 },
],
6,
changeAddress,
);
assert(tx);
assert.strictEqual(tx.outs[0].value, 10234);
assert.strictEqual(
bitcoin.address.fromOutputScript(tx.outs[0].script),
walletSender._getBIP47AddressSend(bip47instanceReceiver.getSerializedPaymentCode(), 0),
);
assert.strictEqual(tx.outs[1].value, 22000);
assert.strictEqual(bitcoin.address.fromOutputScript(tx.outs[1].script), '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS');
const actualFeerate = fee / tx.virtualSize();
assert.strictEqual(Math.round(actualFeerate), 6);
// lets retry, but pretend that a few sender's addresses were used:
walletSender._next_free_payment_code_address_index_send[bip47instanceReceiver.getSerializedPaymentCode()] = 6;
const { tx: tx2 } = walletSender.createTransaction(
utxos,
[
{ address: bip47instanceReceiver.getSerializedPaymentCode(), value: 10234 },
{ address: '13HaCAB4jf7FYSZexJxoczyDDnutzZigjS', value: 22000 },
],
6,
changeAddress,
);
assert(tx2);
assert.strictEqual(
bitcoin.address.fromOutputScript(tx2.outs[0].script),
walletSender._getBIP47AddressSend(bip47instanceReceiver.getSerializedPaymentCode(), 6),
);
});
it('can unwrap addresses to send & receive', () => {
if (!process.env.BIP47_HD_MNEMONIC) {
console.error('process.env.BIP47_HD_MNEMONIC not set, skipped');