forked from sneurlax/bip48
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: match relevant Trezor test vectors
- Loading branch information
Showing
4 changed files
with
350 additions
and
17 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import 'package:bip48/bip48.dart'; | ||
import 'package:coinlib/coinlib.dart'; | ||
|
||
void main() async { | ||
// Initialize coinlib. | ||
await loadCoinlib(); | ||
|
||
// Example 1: Create a 2-of-3 P2SH multisig wallet using xpubs. | ||
print('\nExample 1: 2-of-3 P2SH multisig from xpubs'); | ||
print('==============================================='); | ||
|
||
// These xpubs are from Trezor's test vectors. | ||
// | ||
// See https://github.com/trezor/trezor-firmware/blob/f10dc86da21734fd7be36bbd269da112747df1f3/tests/device_tests/bitcoin/test_getaddress_show.py#L177. | ||
final cosignerXpubs = [ | ||
"xpub6EgGHjcvovyMw8xyoJw9ZRUfjGLS1KUmbjVqMKSNfM6E8hq4EbQ3CpBxfGCPsdxzXtCFuKCxYarzY1TYCG1cmPwq9ep548cM9Ws9rB8V8E8", | ||
"xpub6EexEtC6c2rN5QCpzrL2nUNGDfxizCi3kM1C2Mk5a6PfQs4H3F72C642M3XbnzycvvtD4U6vzn1nYPpH8VUmiREc2YuXP3EFgN1uLTrVEj4", | ||
"xpub6F6Tq7sVLDrhuV3SpvsVKrKofF6Hx7oKxWLFkN6dbepuMhuYueKUnQo7E972GJyeRHqPKu44V1C9zBL6KW47GXjuprhbNrPQahWAFKoL2rN", | ||
]; | ||
|
||
// Create wallet with first xpub. | ||
final xpubWallet = Bip48Wallet( | ||
accountXpub: cosignerXpubs[0], | ||
coinType: 0, // Bitcoin mainnet. | ||
account: 0, | ||
scriptType: Bip48ScriptType.p2shMultisig, | ||
threshold: 2, // 2-of-3 multisig. | ||
totalKeys: 3, | ||
); | ||
|
||
// Add other cosigner xpubs. | ||
xpubWallet.addCosignerXpub(cosignerXpubs[1]); | ||
xpubWallet.addCosignerXpub(cosignerXpubs[2]); | ||
|
||
// Generate first receiving address. | ||
final address0 = xpubWallet.deriveMultisigAddress(0, isChange: false); | ||
print('First receiving address: $address0'); | ||
|
||
// Generate first change address. | ||
final change0 = xpubWallet.deriveMultisigAddress(0, isChange: true); | ||
print('First change address: $change0'); | ||
|
||
// Example 2: Create wallet from master private key. | ||
print('\nExample 2: P2SH multisig from master key'); | ||
print('========================================'); | ||
|
||
// Create from a test seed. | ||
final seedHex = "000102030405060708090a0b0c0d0e0f"; | ||
final masterKey = HDPrivateKey.fromSeed(hexToBytes(seedHex)); | ||
|
||
final privWallet = Bip48Wallet( | ||
masterKey: masterKey, | ||
coinType: 0, | ||
account: 0, | ||
scriptType: Bip48ScriptType.p2shMultisig, | ||
threshold: 2, | ||
totalKeys: 3, | ||
); | ||
|
||
// Get the account xpub for sharing with cosigners. | ||
print('Account xpub to share: ${privWallet.accountXpub}'); | ||
|
||
// Add cosigner xpubs (using example xpubs from above). | ||
privWallet.addCosignerXpub(cosignerXpubs[1]); | ||
privWallet.addCosignerXpub(cosignerXpubs[2]); | ||
|
||
// Generate addresses. | ||
print( | ||
'First receiving address: ${privWallet.deriveMultisigAddress(0, isChange: false)}'); | ||
print( | ||
'First change address: ${privWallet.deriveMultisigAddress(0, isChange: true)}'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,180 @@ | ||
/// Support for doing something awesome. | ||
import 'package:bip48/src/networks/bitcoin.dart'; | ||
import 'package:coinlib/coinlib.dart'; | ||
|
||
/// BIP48 script types. | ||
enum Bip48ScriptType { | ||
p2shMultisig, | ||
p2shP2wshMultisig, | ||
p2wshMultisig, | ||
} | ||
|
||
/// Generate | ||
/// | ||
/// More dartdocs go here. | ||
library; | ||
/// Returns "m/48'/coin_type'/account'/script_index'". | ||
String bip48DerivationPath({ | ||
required int coinType, | ||
required int account, | ||
required Bip48ScriptType scriptType, | ||
}) { | ||
final scriptIndex = switch (scriptType) { | ||
Bip48ScriptType.p2shMultisig => 0, | ||
Bip48ScriptType.p2shP2wshMultisig => 1, | ||
Bip48ScriptType.p2wshMultisig => 2, | ||
}; | ||
return "m/48'/$coinType'/$account'/$scriptIndex'"; | ||
} | ||
|
||
/// A BIP48 wallet that can do M-of-N multisig derivation, either with a | ||
/// private master (so you can sign) or public-only. The underlying | ||
/// coinlib HDKey code uses .derive(...) for child derivation. | ||
class Bip48Wallet { | ||
HDPrivateKey? _accountPrivKey; | ||
HDPublicKey? _accountPubKey; | ||
|
||
final int coinType; | ||
final int account; | ||
final Bip48ScriptType scriptType; | ||
final int threshold; | ||
final int totalKeys; | ||
|
||
final List<HDPublicKey> cosignerKeys = []; | ||
|
||
bool get canSign => _accountPrivKey != null; | ||
|
||
Bip48Wallet({ | ||
HDPrivateKey? masterKey, | ||
String? accountXpub, | ||
required this.coinType, | ||
required this.account, | ||
required this.scriptType, | ||
required this.threshold, | ||
required this.totalKeys, | ||
}) { | ||
if (threshold < 1 || threshold > totalKeys) { | ||
throw ArgumentError( | ||
"Invalid threshold=$threshold for totalKeys=$totalKeys"); | ||
} | ||
if (masterKey == null && accountXpub == null) { | ||
throw ArgumentError("Provide either masterKey or accountXpub."); | ||
} | ||
|
||
final path = bip48DerivationPath( | ||
coinType: coinType, | ||
account: account, | ||
scriptType: scriptType, | ||
); | ||
|
||
if (masterKey != null) { | ||
final acctKey = masterKey.derivePath(path) as HDPrivateKey; | ||
_accountPrivKey = acctKey; | ||
cosignerKeys.add(acctKey.hdPublicKey); | ||
} else { | ||
final pub = HDPublicKey.decode(accountXpub!); | ||
_accountPubKey = pub; | ||
cosignerKeys.add(pub); | ||
} | ||
} | ||
|
||
/// Return the xpub for this account. | ||
String get accountXpub { | ||
if (_accountPrivKey != null) { | ||
final pub = _accountPrivKey!.hdPublicKey; | ||
return pub.encode(bitcoinNetwork.mainnet.pubHDPrefix); | ||
} else { | ||
return _accountPubKey!.encode(bitcoinNetwork.mainnet.pubHDPrefix); | ||
} | ||
} | ||
|
||
/// Add another cosigner xpub to form the M-of-N set. | ||
void addCosignerXpub(String xpub) { | ||
final pub = HDPublicKey.decode(xpub); | ||
cosignerKeys.add(pub); | ||
} | ||
|
||
/// Derive a child public key for the [addressIndex]. BIP48 typically | ||
/// uses non-hardened for change=0/1 and address indices (so pass | ||
/// an integer < HDKey.hardenBit). | ||
HDPublicKey deriveChildPublicKey(int addressIndex, {required bool isChange}) { | ||
final changeIndex = isChange ? 1 : 0; | ||
if (changeIndex >= HDKey.hardenBit) { | ||
throw ArgumentError("changeIndex must be < 0x80000000 (non-hardened)."); | ||
} | ||
if (addressIndex >= HDKey.hardenBit) { | ||
throw ArgumentError("addressIndex must be < 0x80000000 (non-hardened)."); | ||
} | ||
|
||
if (_accountPrivKey != null) { | ||
final HDPrivateKey step1 = _accountPrivKey!.derive(changeIndex); | ||
final HDPrivateKey step2 = step1.derive(addressIndex); | ||
return step2.hdPublicKey; | ||
} else { | ||
// We only have the public key, so we can only do non-hardened derivation. | ||
final HDKey step1 = _accountPubKey!.derive(changeIndex); | ||
final HDKey step2 = step1.derive(addressIndex); | ||
return step2 as HDPublicKey; | ||
} | ||
} | ||
|
||
/// Derive a multi-sig address from all cosigners for the given index. | ||
String deriveMultisigAddress(int addressIndex, {required bool isChange}) { | ||
if (cosignerKeys.length < totalKeys) { | ||
throw StateError( | ||
"Not enough cosigners added (${cosignerKeys.length} < $totalKeys)"); | ||
} | ||
|
||
if (cosignerKeys.length < totalKeys) { | ||
throw StateError( | ||
"Not enough cosigners added (${cosignerKeys.length} < $totalKeys)"); | ||
} | ||
|
||
final childKeys = <ECPublicKey>[]; | ||
final cIndex = isChange ? 1 : 0; | ||
|
||
if (cIndex >= HDKey.hardenBit) { | ||
throw ArgumentError("change index must be < 0x80000000"); | ||
} | ||
if (addressIndex >= HDKey.hardenBit) { | ||
throw ArgumentError("address index must be < 0x80000000"); | ||
} | ||
|
||
// Derive child keys, maintaining original order. | ||
// | ||
// Originally, child keys were sorted according to BIP67. However, this | ||
// broke the tests, so we use the original order here in order to strictly | ||
// adhere to Trezor's vectors. | ||
for (final cosigner in cosignerKeys) { | ||
final step1 = cosigner.derive(cIndex); | ||
final step2 = step1.derive(addressIndex); | ||
final cPub = (step2 as HDPublicKey).publicKey; | ||
childKeys.add(cPub); | ||
} | ||
|
||
final script = Script([ | ||
ScriptOp.fromNumber(threshold), | ||
...childKeys.map((pk) => ScriptPushData(pk.data)), | ||
ScriptOp.fromNumber(childKeys.length), | ||
ScriptOpCode.fromName("CHECKMULTISIG"), | ||
]); | ||
|
||
switch (scriptType) { | ||
case Bip48ScriptType.p2shMultisig: | ||
return P2SHAddress.fromRedeemScript( | ||
script, | ||
version: bitcoinNetwork.mainnet.p2shPrefix, | ||
).toString(); | ||
|
||
export 'src/bip48_base.dart'; | ||
case Bip48ScriptType.p2shP2wshMultisig: | ||
final witnessProg = P2WSH.fromWitnessScript(script); | ||
return P2SHAddress.fromRedeemScript( | ||
witnessProg.script, | ||
version: bitcoinNetwork.mainnet.p2shPrefix, | ||
).toString(); | ||
|
||
// TODO: Export any libraries intended for clients of this package. | ||
case Bip48ScriptType.p2wshMultisig: | ||
return P2WSHAddress.fromWitnessScript( | ||
script, | ||
hrp: bitcoinNetwork.mainnet.bech32Hrp, | ||
).toString(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,111 @@ | ||
import 'package:bip48/bip48.dart'; | ||
import 'package:coinlib/coinlib.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
group('A group of tests', () { | ||
final awesome = Awesome(); | ||
group("BIP48 P2SH Multisig Tests", () { | ||
setUpAll(() async { | ||
await loadCoinlib(); | ||
}); | ||
|
||
test("Trezor test vector - P2SH 2-of-3 multisig", () { | ||
// From Trezor test vectors. | ||
// | ||
// See https://github.com/trezor/trezor-firmware/blob/f10dc86da21734fd7be36bbd269da112747df1f3/tests/device_tests/bitcoin/test_getaddress_show.py#L177. | ||
final pubkeys = [ | ||
"xpub6EgGHjcvovyMw8xyoJw9ZRUfjGLS1KUmbjVqMKSNfM6E8hq4EbQ3CpBxfGCPsdxzXtCFuKCxYarzY1TYCG1cmPwq9ep548cM9Ws9rB8V8E8", | ||
"xpub6EexEtC6c2rN5QCpzrL2nUNGDfxizCi3kM1C2Mk5a6PfQs4H3F72C642M3XbnzycvvtD4U6vzn1nYPpH8VUmiREc2YuXP3EFgN1uLTrVEj4", | ||
"xpub6F6Tq7sVLDrhuV3SpvsVKrKofF6Hx7oKxWLFkN6dbepuMhuYueKUnQo7E972GJyeRHqPKu44V1C9zBL6KW47GXjuprhbNrPQahWAFKoL2rN", | ||
]; | ||
|
||
final wallet = Bip48Wallet( | ||
accountXpub: pubkeys[0], // Start with first key. | ||
coinType: 0, // Bitcoin mainnet. | ||
account: 0, // First account. | ||
scriptType: Bip48ScriptType.p2shMultisig, | ||
threshold: 2, // 2-of-3. | ||
totalKeys: 3, | ||
); | ||
|
||
// Add other two cosigner xpubs. | ||
wallet.addCosignerXpub(pubkeys[1]); | ||
wallet.addCosignerXpub(pubkeys[2]); | ||
|
||
setUp(() { | ||
// Additional setup goes here. | ||
// Test external (non-change) address at index 0. | ||
expect( | ||
wallet.deriveMultisigAddress(0, isChange: false), | ||
"33TU5DyVi2kFSGQUfmZxNHgPDPqruwdesY", | ||
); | ||
}); | ||
|
||
test('First Test', () { | ||
expect(awesome.isAwesome, isTrue); | ||
test("Path validation and derivation", () { | ||
final seedHex = "000102030405060708090a0b0c0d0e0f"; | ||
final masterKey = HDPrivateKey.fromSeed(hexToBytes(seedHex)); | ||
|
||
final path = bip48DerivationPath( | ||
coinType: 0, | ||
account: 0, | ||
scriptType: Bip48ScriptType.p2shMultisig, | ||
); | ||
expect(path, "m/48'/0'/0'/0'"); | ||
|
||
// Verify we can derive through this path | ||
final wallet = Bip48Wallet( | ||
masterKey: masterKey, | ||
coinType: 0, | ||
account: 0, | ||
scriptType: Bip48ScriptType.p2shMultisig, | ||
threshold: 2, | ||
totalKeys: 3, | ||
); | ||
|
||
expect(wallet.canSign, true); | ||
|
||
// Add the two cosigners from Trezor's test vector. | ||
wallet.addCosignerXpub( | ||
"xpub6EexEtC6c2rN5QCpzrL2nUNGDfxizCi3kM1C2Mk5a6PfQs4H3F72C642M3XbnzycvvtD4U6vzn1nYPpH8VUmiREc2YuXP3EFgN1uLTrVEj4"); | ||
wallet.addCosignerXpub( | ||
"xpub6F6Tq7sVLDrhuV3SpvsVKrKofF6Hx7oKxWLFkN6dbepuMhuYueKUnQo7E972GJyeRHqPKu44V1C9zBL6KW47GXjuprhbNrPQahWAFKoL2rN"); | ||
|
||
// Now test address derivation. | ||
expect( | ||
() => wallet.deriveMultisigAddress(HDKey.hardenBit, isChange: false), | ||
throwsArgumentError, | ||
); | ||
expect( | ||
() => wallet.deriveMultisigAddress(0, isChange: true), | ||
returnsNormally, | ||
); | ||
}); | ||
|
||
test("Wallet construction validation", () { | ||
final seedHex = "000102030405060708090a0b0c0d0e0f"; | ||
final masterKey = HDPrivateKey.fromSeed(hexToBytes(seedHex)); | ||
|
||
// Invalid M-of-N. | ||
expect( | ||
() => Bip48Wallet( | ||
masterKey: masterKey, | ||
coinType: 0, | ||
account: 0, | ||
scriptType: Bip48ScriptType.p2shMultisig, | ||
threshold: 4, // Can't have 4-of-3. | ||
totalKeys: 3, | ||
), | ||
throwsArgumentError, | ||
); | ||
|
||
// Must provide either master key or xpub. | ||
expect( | ||
() => Bip48Wallet( | ||
coinType: 0, | ||
account: 0, | ||
scriptType: Bip48ScriptType.p2shMultisig, | ||
threshold: 2, | ||
totalKeys: 3, | ||
), | ||
throwsArgumentError, | ||
); | ||
}); | ||
}); | ||
} |