Skip to content
Open
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
14 changes: 14 additions & 0 deletions lib/src/payment_address.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ class PaymentAddress {
Uint8List hashSharedSecret() =>
SHA256Digest().process(getSharedSecret().ecdhSecret());

/// Returns the compressed public key bytes for the derived send address.
/// Can be used by callers to construct any address type (P2PKH, P2WPKH, P2TR).
Uint8List getDerivedSendPublicKey() {
final sum = getECPoint() + sG();
return sum!.getEncoded(true);
}

bitcoindart.ECPair _getSendAddressKeyPair() {
final sum = getECPoint() + sG();
return bitcoindart.ECPair.fromPublicKey(
Expand Down Expand Up @@ -111,6 +118,13 @@ class PaymentAddress {
return p2pkh.data.address!;
}

/// Returns the compressed public key bytes for the derived receive address.
/// Can be used by callers to construct any address type (P2PKH, P2WPKH, P2TR).
Uint8List getDerivedReceivePublicKey() {
final pair = getReceiveAddressKeyPair();
return pair.publicKey;
}

BigInt _addSecp256k1(BigInt b1, BigInt b2) {
final BigInt value = b1 + b2;

Expand Down
41 changes: 37 additions & 4 deletions lib/src/payment_code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class PaymentCode {

static const int SAMOURAI_FEATURE_BYTE = 79;
static const int SAMOURAI_SEGWIT_BIT = 0;
static const int SAMOURAI_TAPROOT_BIT = 1;

late String _paymentCodeString;
late final bip32.BIP32 _bip32Node;
Expand Down Expand Up @@ -65,20 +66,30 @@ class PaymentCode {
_getBip32NetworkTypeFrom(networkType),
);

final bool isSamouraiSegwit = Util.isBitSet(
final bool hasTaproot = Util.isBitSet(
payload[SAMOURAI_FEATURE_BYTE],
SAMOURAI_TAPROOT_BIT,
);
final bool hasSegwit = Util.isBitSet(
payload[SAMOURAI_FEATURE_BYTE],
SAMOURAI_SEGWIT_BIT,
);

_paymentCodeString =
isSamouraiSegwit ? _makeSamouraiPaymentCode() : _makeV1();
if (hasTaproot) {
_paymentCodeString = _makeTaprootPaymentCode();
} else if (hasSegwit) {
_paymentCodeString = _makeSamouraiPaymentCode();
} else {
_paymentCodeString = _makeV1();
}
}

// initialize payment code given a bip32 object
PaymentCode.fromBip32Node(
bip32.BIP32 bip32Node, {
required this.networkType,
required bool shouldSetSegwitBit,
bool shouldSetTaprootBit = false,
}) {
if (bip32Node.network.wif != networkType.wif ||
bip32Node.network.bip32.public != networkType.bip32.public ||
Expand All @@ -87,9 +98,12 @@ class PaymentCode {
"BIP32 network info does not match provided networkType info");
}
_bip32Node = bip32Node;
// Always build v1 first (sets _paymentCodeString), then upgrade if needed.
_paymentCodeString = _makeV1();

if (shouldSetSegwitBit) {
if (shouldSetTaprootBit) {
_paymentCodeString = _makeTaprootPaymentCode();
} else if (shouldSetSegwitBit) {
_paymentCodeString = _makeSamouraiPaymentCode();
}
}
Expand Down Expand Up @@ -131,6 +145,11 @@ class PaymentCode {
SAMOURAI_SEGWIT_BIT,
);

bool isTaprootEnabled() => Util.isBitSet(
getPayload()[SAMOURAI_FEATURE_BYTE],
SAMOURAI_TAPROOT_BIT,
);

Uint8List getPubKey() => _bip32Node.publicKey;

Uint8List getChain() => _bip32Node.chainCode;
Expand Down Expand Up @@ -268,6 +287,20 @@ class PaymentCode {
return paymentCode.toBase58Check;
}

String _makeTaprootPaymentCode() {
final payload = getPayload();
// set segwit bit (taproot implies segwit)
payload[SAMOURAI_FEATURE_BYTE] =
Util.setBit(payload[SAMOURAI_FEATURE_BYTE], SAMOURAI_SEGWIT_BIT);
// set taproot bit
payload[SAMOURAI_FEATURE_BYTE] =
Util.setBit(payload[SAMOURAI_FEATURE_BYTE], SAMOURAI_TAPROOT_BIT);
Uint8List paymentCode = Uint8List(PAYLOAD_LEN + 1);
paymentCode[0] = 0x47;
Util.copyBytes(payload, 0, paymentCode, 1, PAYLOAD_LEN);
return paymentCode.toBase58Check;
}

/// Use at own risk. Not fully tested
bool isValid() {
try {
Expand Down
233 changes: 233 additions & 0 deletions test/taproot_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:bip47/bip47.dart';
import 'package:bitcoindart/bitcoindart.dart' as bitcoindart;
import 'package:test/test.dart';

const String kPath = "m/47'/0'/0'";

const String kSeedAlice =
"response seminar brave tip suit recall often sound stick owner lottery mot"
"ion";
const String kSeedBob =
"reward upper indicate eight swift arch injury crystal super wrestle alread"
"y dentist";

bip32.NetworkType get btcNetwork => bip32.NetworkType(
wif: bitcoindart.bitcoin.wif,
bip32: bip32.Bip32Type(
public: bitcoindart.bitcoin.bip32.public,
private: bitcoindart.bitcoin.bip32.private,
),
);

void main() {
late bip32.BIP32 aliceNode;
late bip32.BIP32 bobNode;

setUpAll(() {
final aliceSeed = bip39.mnemonicToSeed(kSeedAlice);
final aliceRoot = bip32.BIP32.fromSeed(aliceSeed, btcNetwork);
aliceNode = aliceRoot.derivePath(kPath);

final bobSeed = bip39.mnemonicToSeed(kSeedBob);
final bobRoot = bip32.BIP32.fromSeed(bobSeed, btcNetwork);
bobNode = bobRoot.derivePath(kPath);
});

group('PaymentCode taproot feature bit', () {
test('fromBip32Node with shouldSetTaprootBit sets both bits', () {
final code = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: true,
shouldSetTaprootBit: true,
);
expect(code.isTaprootEnabled(), isTrue);
expect(code.isSegWitEnabled(), isTrue);
});

test('fromBip32Node without taproot does not set taproot bit', () {
final code = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: true,
);
expect(code.isTaprootEnabled(), isFalse);
expect(code.isSegWitEnabled(), isTrue);
});

test('fromBip32Node with neither flag sets neither bit', () {
final code = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: false,
);
expect(code.isTaprootEnabled(), isFalse);
expect(code.isSegWitEnabled(), isFalse);
});

test('round-trip: encode taproot code, parse back, verify bits', () {
final original = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: true,
shouldSetTaprootBit: true,
);
final codeString = original.toString();

final parsed = PaymentCode.fromPaymentCode(
codeString,
networkType: bitcoindart.bitcoin,
);
expect(parsed.isTaprootEnabled(), isTrue);
expect(parsed.isSegWitEnabled(), isTrue);
});

test('round-trip: segwit-only code preserved', () {
final original = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: true,
);
final parsed = PaymentCode.fromPaymentCode(
original.toString(),
networkType: bitcoindart.bitcoin,
);
expect(parsed.isSegWitEnabled(), isTrue);
expect(parsed.isTaprootEnabled(), isFalse);
});

test('taproot and non-taproot codes have same notification address', () {
final taproot = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: true,
shouldSetTaprootBit: true,
);
final v1 = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: false,
);
expect(
taproot.notificationAddressP2PKH(),
equals(v1.notificationAddressP2PKH()),
);
});
});

group('PaymentAddress derived public keys', () {
test('getDerivedSendPublicKey returns 33-byte compressed key', () {
final bobCode = PaymentCode.fromBip32Node(
bobNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: false,
);

final paymentAddress = PaymentAddress(
bip32Node: aliceNode.derive(0),
paymentCode: bobCode,
networkType: bitcoindart.bitcoin,
index: 0,
);

final pubKey = paymentAddress.getDerivedSendPublicKey();
expect(pubKey.length, equals(33));
expect(pubKey[0] == 0x02 || pubKey[0] == 0x03, isTrue);
});

test(
'send and receive derive same public key '
'(Alice sends to Bob, Bob receives from Alice)', () {
final aliceCode = PaymentCode.fromBip32Node(
aliceNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: false,
);
final bobCode = PaymentCode.fromBip32Node(
bobNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: false,
);

// Alice sends to Bob: uses Alice's private key + Bob's public code
final aliceSendAddr = PaymentAddress(
bip32Node: aliceNode.derive(0),
paymentCode: bobCode,
networkType: bitcoindart.bitcoin,
index: 0,
);

// Bob receives from Alice: uses Bob's private key + Alice's public code
final bobReceiveAddr = PaymentAddress(
bip32Node: bobNode.derive(0),
paymentCode: aliceCode,
networkType: bitcoindart.bitcoin,
index: 0,
);

final sendPubKey = aliceSendAddr.getDerivedSendPublicKey();
final receivePubKey = bobReceiveAddr.getDerivedReceivePublicKey();

expect(sendPubKey, equals(receivePubKey));
});

test('getDerivedSendPublicKey matches P2PKH address derivation', () {
final bobCode = PaymentCode.fromBip32Node(
bobNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: false,
);

final paymentAddress = PaymentAddress(
bip32Node: aliceNode.derive(0),
paymentCode: bobCode,
networkType: bitcoindart.bitcoin,
index: 0,
);

// The P2PKH address should correspond to the derived public key
final p2pkhAddress = paymentAddress.getSendAddressP2PKH();
final derivedPubKey = paymentAddress.getDerivedSendPublicKey();

// Verify by constructing P2PKH from the derived key directly
final pair = bitcoindart.ECPair.fromPublicKey(
derivedPubKey,
network: bitcoindart.bitcoin,
);
final p2pkh = bitcoindart.P2PKH(
data: bitcoindart.PaymentData(pubkey: pair.publicKey),
network: bitcoindart.bitcoin,
);

expect(p2pkh.data.address, equals(p2pkhAddress));
});

test('derived keys at different indices produce different addresses', () {
final bobCode = PaymentCode.fromBip32Node(
bobNode,
networkType: bitcoindart.bitcoin,
shouldSetSegwitBit: false,
);

final addr0 = PaymentAddress(
bip32Node: aliceNode.derive(0),
paymentCode: bobCode,
networkType: bitcoindart.bitcoin,
index: 0,
);
final addr1 = PaymentAddress(
bip32Node: aliceNode.derive(0),
paymentCode: bobCode,
networkType: bitcoindart.bitcoin,
index: 1,
);

expect(
addr0.getDerivedSendPublicKey(),
isNot(equals(addr1.getDerivedSendPublicKey())),
);
});
});
}