Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[master] enable bip86 signing, cleanup and add tests #9

Merged
merged 1 commit into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/snap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@metamask/snaps-cli": "^0.32.2",
"@metamask/snaps-ui": "^0.32.2",
"bip32": "^4.0.0",
"bip174": "^2.1.1",
"bitcoinjs-lib": "^6.1.5",
"bitcoinjs-message": "^2.2.0",
"bn.js": "^5.2.1",
Expand All @@ -80,4 +81,4 @@
"prettier": "^2.7.1",
"through2": "^4.0.2"
}
}
}
18 changes: 17 additions & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,24 @@
"1'"
],
"curve": "secp256k1"
},
{
"path": [
"m",
"86'",
"0'"
],
"curve": "secp256k1"
},
{
"path": [
"m",
"86'",
"1'"
],
"curve": "secp256k1"
}
]
},
"manifestVersion": "0.1"
}
}
60 changes: 35 additions & 25 deletions packages/snap/src/bitcoin/PsbtValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { BitcoinNetwork } from '../interface';
import { PsbtHelper } from '../bitcoin/PsbtHelper';
import { fromHdPathToObj } from './cryptoPath';
import { PsbtValidateErrors, SnapError } from "../errors";
import { isTaprootInput } from 'bitcoinjs-lib/src/psbt/bip371';
import { tapInputHasHDKey, tapOutputHasHDKey } from './tapSigner';

const BITCOIN_MAINNET_COIN_TYPE = 0;
const BITCOIN_TESTNET_COIN_TYPE = 1;
Expand All @@ -29,8 +31,8 @@ export class PsbtValidator {
this.psbtHelper = new PsbtHelper(this.tx, network);
}

get coinType(){
return this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAINNET_COIN_TYPE: BITCOIN_TESTNET_COIN_TYPE;
get coinType() {
return this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAINNET_COIN_TYPE : BITCOIN_TESTNET_COIN_TYPE;
}

allInputsHaveRawTxHex() {
Expand All @@ -43,12 +45,14 @@ export class PsbtValidator {

everyInputMatchesNetwork() {
const result = this.tx.data.inputs.every(input => {
if (!input.bip32Derivation || input.bip32Derivation.length === 0) {
// ignore if we don't have the derivation path
return true;
if (isTaprootInput(input)) {
return input.tapBip32Derivation.every(derivation => {
const { coinType } = fromHdPathToObj(derivation.path);
return Number(coinType) === this.coinType;
});
} else {
return input.bip32Derivation.every(derivation => {
const {coinType} = fromHdPathToObj(derivation.path);
const { coinType } = fromHdPathToObj(derivation.path);
return Number(coinType) === this.coinType;
});
}
Expand All @@ -62,15 +66,20 @@ export class PsbtValidator {
everyOutputMatchesNetwork() {
const addressPattern = this.snapNetwork === BitcoinNetwork.Main ? BITCOIN_MAIN_NET_ADDRESS_PATTERN : BITCOIN_TEST_NET_ADDRESS_PATTERN;
const result = this.tx.data.outputs.every((output, index) => {
if(output.bip32Derivation){
return output.bip32Derivation.every(derivation => {
const {coinType} = fromHdPathToObj(derivation.path)
return Number(coinType) === this.coinType
})
} else {
const address = this.tx.txOutputs[index].address;
return addressPattern.test(address);
}
if (output.tapBip32Derivation) {
return output.tapBip32Derivation.every(derivation => {
const { coinType } = fromHdPathToObj(derivation.path)
return Number(coinType) === this.coinType
})
} else if (output.bip32Derivation) {
return output.bip32Derivation.every(derivation => {
const { coinType } = fromHdPathToObj(derivation.path)
return Number(coinType) === this.coinType
})
} else {
const address = this.tx.txOutputs[index].address;
return addressPattern.test(address);
}
})

if (!result) {
Expand All @@ -82,9 +91,8 @@ export class PsbtValidator {
allInputsBelongToCurrentAccount(accountSigner: AccountSigner) {
const result = this.tx.txInputs.every((_, index) => {
const input = checkForInput(this.tx.data.inputs, index);
if (!input.bip32Derivation || input.bip32Derivation.length === 0) {
// ignore if we don't have the derivation path
return true;
if (isTaprootInput(input)) {
return tapInputHasHDKey(input, accountSigner);
} else {
return this.tx.inputHasHDKey(index, accountSigner);
}
Expand All @@ -97,7 +105,9 @@ export class PsbtValidator {

changeAddressBelongsToCurrentAccount(accountSigner: AccountSigner) {
const result = this.tx.data.outputs.every((output, index) => {
if (output.bip32Derivation) {
if (output.tapBip32Derivation) {
return tapOutputHasHDKey(output, accountSigner);
} else if (output.bip32Derivation) {
return this.tx.outputHasHDKey(index, accountSigner);
}
return true;
Expand Down Expand Up @@ -137,12 +147,12 @@ export class PsbtValidator {
this.error = null;

this.allInputsHaveRawTxHex() &&
this.everyInputMatchesNetwork() &&
this.everyOutputMatchesNetwork() &&
this.allInputsBelongToCurrentAccount(accountSigner) &&
this.changeAddressBelongsToCurrentAccount(accountSigner) &&
this.feeUnderThreshold() &&
this.witnessUtxoValueMatchesNoneWitnessOnes();
this.everyInputMatchesNetwork() &&
this.everyOutputMatchesNetwork() &&
this.allInputsBelongToCurrentAccount(accountSigner) &&
this.changeAddressBelongsToCurrentAccount(accountSigner) &&
this.feeUnderThreshold() &&
this.witnessUtxoValueMatchesNoneWitnessOnes();

if (this.error) {
throw this.error
Expand Down
82 changes: 67 additions & 15 deletions packages/snap/src/bitcoin/__tests__/PsbtValidator.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as bip32 from 'bip32';
import BIP32Factory from 'bip32';
import { networks, Psbt } from 'bitcoinjs-lib';
import { PsbtValidator } from '../PsbtValidator';
import { AccountSigner } from '../index';
import { BitcoinNetwork } from '../../interface';
import { psbtFixture } from './fixtures/psbt';
import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";

const getAccountSigner = () => {
const testPrivateAccountKey = "tprv8gwYx7tEWpLxdJhEa7R8ofchqzRgme6iiuyJpegZ71XNhnAqeMjT6GV4wm3jqsUjXgXj99GB4kDminso5kxnLa6VXt3WVRzfmhbDSrfbCDv";
const accountNode = bip32.fromBase58(testPrivateAccountKey, networks.testnet);
const accountNode = BIP32Factory(ecc).fromBase58(testPrivateAccountKey, networks.testnet);
return new AccountSigner(accountNode, Buffer.from("f812d139", 'hex'));
}

Expand All @@ -27,21 +28,36 @@ describe('psbtValidator', () => {

it('should throw error when not all inputs have nonWitnessUtxo', () => {
const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => {psbtValidator.validate(signer)}).toThrowError('Not all inputs have prev Tx raw hex');
expect(() => { psbtValidator.validate(signer) }).toThrowError('Not all inputs have prev Tx raw hex');
});

it('should throw error when not all inputs matches network', () => {
psbt.updateInput(0,{
psbt.updateInput(0, {
nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo,
bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation,
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Main);
expect(() => {psbtValidator.validate(signer)}).toThrowError('Not every input matches network');
expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every input matches network');
});

it('should throw error when not all inputs matches network (taproot)', () => {
psbt.updateInput(0, {
nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo,
tapBip32Derivation: [{
masterFingerprint: Buffer.alloc(4, 0),
pubkey: Buffer.alloc(32, 0),
path: "m/86'/1'/0'/1/10",
leafHashes: [],
}],
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Main);
expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every input matches network');
});

it('should throw error when not all outputs matches network', () => {
psbt.updateInput(0,{
psbt.updateInput(0, {
nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo,
bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation,
})
Expand All @@ -56,11 +72,31 @@ describe('psbtValidator', () => {
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => {psbtValidator.validate(signer)}).toThrowError('Not every output matches network');
expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every output matches network');
});

it('should throw error when not all outputs matches network (taproot)', () => {
psbt.updateInput(0, {
nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo,
bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation,
})
psbt.addOutput({
script: Buffer.from('0014198d799580d87fb6c0341b1e9619a20ef47cd5f8', 'hex'),
value: 10000,
tapBip32Derivation: [{
masterFingerprint: Buffer.alloc(4, 0),
pubkey: Buffer.alloc(32, 0),
path: `m/86'/0'/0'/1/1`,
leafHashes: [],
}],
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => { psbtValidator.validate(signer) }).toThrowError('Not every output matches network');
});

it('should throw error when not all inputs belong to current account', () => {
psbt.updateInput(0,{
psbt.updateInput(0, {
nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo,
bip32Derivation: [{
masterFingerprint: Buffer.from('d1f83912', 'hex'),
Expand All @@ -70,11 +106,27 @@ describe('psbtValidator', () => {
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => {psbtValidator.validate(signer)}).toThrowError('Not all inputs belongs to current account');
expect(() => { psbtValidator.validate(signer) }).toThrowError('Not all inputs belongs to current account');
});

it('should throw error when not all inputs belong to current account (taproot)', () => {
psbt.updateInput(0, {
nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo,
tapInternalKey: Buffer.alloc(32, 0),
tapBip32Derivation: [{
masterFingerprint: Buffer.alloc(4, 0),
pubkey: Buffer.alloc(32, 0),
path: `m/86'/1'/0'/1/0`,
leafHashes: [],
}],
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => { psbtValidator.validate(signer) }).toThrowError('Not all inputs belongs to current account');
});

it('should throw error when not all change addresses belong to current account', () => {
psbt.updateInput(0,{
psbt.updateInput(0, {
nonWitnessUtxo: psbtFixture.data.inputs[0].nonWitnessUtxo,
bip32Derivation: psbtFixture.data.inputs[0].bip32Derivation,
})
Expand All @@ -87,7 +139,7 @@ describe('psbtValidator', () => {
})

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => {psbtValidator.validate(signer)}).toThrowError(`Change address doesn't belongs to current account`);
expect(() => { psbtValidator.validate(signer) }).toThrowError(`Change address doesn't belongs to current account`);
});

it('should throw error when fee is too high', () => {
Expand All @@ -105,7 +157,7 @@ describe('psbtValidator', () => {
psbt.addOutputs(psbtFixture.tx.outputs);

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => {psbtValidator.validate(signer)}).toThrowError('Too much fee');
expect(() => { psbtValidator.validate(signer) }).toThrowError('Too much fee');
});

it('should throw error given witnessUtxo value not equals to nonWitnessUtxo value', () => {
Expand All @@ -123,11 +175,11 @@ describe('psbtValidator', () => {
psbt.addOutputs(psbtFixture.tx.outputs);

const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);
expect(() => {psbtValidator.validate(signer)}).toThrowError('Transaction input amount not match');
expect(() => { psbtValidator.validate(signer) }).toThrowError('Transaction input amount not match');
});

it('should return true given a valid psbt', function() {
const psbt = Psbt.fromBase64(psbtFixture.base64, { network: networks.testnet})
it('should return true given a valid psbt', function () {
const psbt = Psbt.fromBase64(psbtFixture.base64, { network: networks.testnet })
const psbtValidator = new PsbtValidator(psbt, BitcoinNetwork.Test);

expect(psbtValidator.validate(signer)).toBe(true);
Expand Down
2 changes: 1 addition & 1 deletion packages/snap/src/bitcoin/__tests__/fixtures/psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@ export const psbtFixture = {
],
},
base64: 'cHNidP8BAHECAAAAASpPo+Jtjb8ce88iDgUe9MdBl/N0RXzOyTt6bYRF4KxgAAAAAAD/////AkANAwAAAAAAFgAUBbP+LIMGzIE0s5pdBRLRb/T3kYaazQsAAAAAABYAFDUcL8Uq83TUCsXnr18EDVw08zQ9AAAAAAABAP1UAQIAAAAAAQIbzNzgXMu2XcHbu/VK6Tv2aYkp3WBu0PijNRnjyvh1eQEAAAAA/////wL6QmQPXjZt03YXBX2QbexW+etvDRpeUJE+cz7D5ardAAAAAAD/////Afz3DgAAAAAAFgAUvApnUSw4MVXYWN2ZqWf3hCAWGsoCSDBFAiEA+axvhH4bFn2mSxo6xzybYtrAjdpG0YzlqBam0UNaE3kCIA0lg97qGHi0rKC7hWQXnMSnWbaCII6nGpHErzpoSHNMASEDSB6PkHcBABG+ayUezMfaQN0i6+DO4DwxtF+nbuWWp+ICRzBEAiAYgnrWAOCiD55kZsBSiX/UNMnDsmhcFzKno/6T1nP92gIgPtzzZuB0FjdMrkpzWkDZurYJR7MBcRnszVgqyjX5xEsBIQMR9PpNCfA5TzCasyKhJsp1vevr907O2Kru24aJS/h08QAAAAABAR/89w4AAAAAABYAFLwKZ1EsODFV2Fjdmaln94QgFhrKIgYDSB6PkHcBABG+ayUezMfaQN0i6+DO4DwxtF+nbuWWp+IY+BLROVQAAIABAACAAAAAgAEAAAAKAAAAAAAiAgJgiLaAsqyAi3BUwv3EgxEuXiYfGOFeDgCax2fzYa/sZBj4EtE5VAAAgAEAAIAAAACAAQAAAAsAAAAA',

};
2 changes: 1 addition & 1 deletion packages/snap/src/bitcoin/__tests__/getNetwork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ describe('getNetwork', () => {
});

it('should throw error given invalid network', () => {
expect(() => {getNetwork('litcoin' as any)}).toThrowError('Network net exist')
expect(() => { getNetwork('litcoin' as any) }).toThrowError('Network net exist')
});
});
Loading