Skip to content
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
60 changes: 60 additions & 0 deletions pkg/bitcoin/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ var signaturePlaceholder = make([]byte, 72)
// Compressed public keys have always 33 bytes.
var publicKeyPlaceholder = make([]byte, 33)

// Taproot key-path signatures use the 64-byte BIP-340 Schnorr encoding when
// SIGHASH_DEFAULT is used.
var taprootKeyPathSignaturePlaceholder = make([]byte, 64)

// TransactionSizeEstimator is a component allowing to estimate the size
// of a Bitcoin transaction of the provided shape, without constructing it.
type TransactionSizeEstimator struct {
Expand Down Expand Up @@ -75,6 +79,40 @@ func (tse *TransactionSizeEstimator) AddPublicKeyHashInputs(
return tse
}

// AddPublicKeyScriptInput adds an input spending an output locked by the
// provided direct-key public key script. P2PKH, P2WPKH, and P2TR key-path
// spends are supported. If the estimator already errored out during previous
// actions, this method does nothing.
func (tse *TransactionSizeEstimator) AddPublicKeyScriptInput(
publicKeyScript Script,
) *TransactionSizeEstimator {
if tse.err != nil {
return tse
}

switch GetScriptType(publicKeyScript) {
case P2PKHScript:
return tse.AddPublicKeyHashInputs(1, false)
case P2WPKHScript:
return tse.AddPublicKeyHashInputs(1, true)
case P2TRScript:
tse.internal.AddTxIn(
wire.NewTxIn(
wire.NewOutPoint((*chainhash.Hash)(&[32]byte{}), 0),
nil,
wire.TxWitness{taprootKeyPathSignaturePlaceholder},
),
)
default:
tse.err = fmt.Errorf(
"unsupported direct-key input script type: [%v]",
GetScriptType(publicKeyScript),
)
}

return tse
}

// AddScriptHashInputs adds the provided count of P2WSH (isWitness is true)
// or P2SH (isWitness is false) inputs to the estimation. The redeemScriptLength
// argument should be used to pass the byte size of the plain-text redeem
Expand Down Expand Up @@ -201,6 +239,28 @@ func (tse *TransactionSizeEstimator) AddScriptHashOutputs(
return tse
}

// AddOutputScript adds an output locked by the provided public key script. If
// the estimator already errored out during previous actions, this method does
// nothing.
func (tse *TransactionSizeEstimator) AddOutputScript(
publicKeyScript Script,
) *TransactionSizeEstimator {
if tse.err != nil {
return tse
}

if len(publicKeyScript) == 0 {
tse.err = fmt.Errorf("output public key script is empty")
return tse
}

tse.internal.AddTxOut(
wire.NewTxOut(0, append([]byte(nil), publicKeyScript...)),
)

return tse
}

// VirtualSize returns the virtual size of the transaction whose shape was
// provided to the estimator. If any errors occurred while building the
// transaction shape, the first error will be returned.
Expand Down
31 changes: 31 additions & 0 deletions pkg/bitcoin/estimator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import (
)

func TestTransactionSizeEstimator_VirtualSize(t *testing.T) {
taprootScript, err := PayToTaproot([32]byte{0x01})
if err != nil {
t.Fatal(err)
}
witnessPublicKeyHashScript, err := PayToWitnessPublicKeyHash([20]byte{0x02})
if err != nil {
t.Fatal(err)
}

var tests = map[string]struct {
estimator *TransactionSizeEstimator
expectedVirtualSize int
Expand Down Expand Up @@ -63,6 +72,28 @@ func TestTransactionSizeEstimator_VirtualSize(t *testing.T) {
AddScriptHashOutputs(1, true),
expectedVirtualSize: 250,
},
"1 P2TR key-path input and 1 P2TR output": {
estimator: NewTransactionSizeEstimator().
AddPublicKeyScriptInput(taprootScript).
AddOutputScript(taprootScript),
expectedVirtualSize: 111,
},
"2 P2TR key-path inputs and 1 P2TR output": {
estimator: NewTransactionSizeEstimator().
AddPublicKeyScriptInput(taprootScript).
AddPublicKeyScriptInput(taprootScript).
AddOutputScript(taprootScript),
expectedVirtualSize: 169,
},
"1 P2TR input and mixed P2TR and P2WPKH outputs": {
estimator: NewTransactionSizeEstimator().
AddPublicKeyScriptInput(taprootScript).
AddOutputScript(taprootScript).
AddOutputScript(taprootScript).
AddOutputScript(witnessPublicKeyHashScript).
AddOutputScript(witnessPublicKeyHashScript),
expectedVirtualSize: 216,
},
}

for testName, test := range tests {
Expand Down
83 changes: 64 additions & 19 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,7 @@ func pastNewWalletRegisteredEvents(
bridge any,
pastLegacyEvents pastNewWalletRegisteredEventsFn,
) ([]*tbtc.NewWalletRegisteredEvent, error) {
convertedEvents, err := pastNewWalletRegisteredV2Events(
v2Events, err := pastNewWalletRegisteredV2Events(
startBlock,
endBlock,
walletID,
Expand All @@ -1939,28 +1939,63 @@ func pastNewWalletRegisteredEvents(
return nil, err
}

// Fallback for legacy deployments that do not emit NewWalletRegisteredV2.
if len(convertedEvents) == 0 && len(walletID) == 0 {
legacyEvents, err := pastLegacyEvents(
startBlock,
endBlock,
ecdsaWalletID,
walletPublicKeyHash,
)
if err != nil {
return nil, err
legacyEvents, err := pastLegacyEvents(
startBlock,
endBlock,
ecdsaWalletID,
walletPublicKeyHash,
)
if err != nil {
return nil, err
}

convertedEvents := make(
[]*tbtc.NewWalletRegisteredEvent,
0,
len(v2Events)+len(legacyEvents),
)
seenRegistrations := make(map[[20]byte]struct{})

appendUnique := func(event *tbtc.NewWalletRegisteredEvent) {
// The Bridge keys wallet state by public key hash. Compatibility legacy
// events cannot carry a FROST wallet's canonical x-only wallet ID, so
// the public key hash is the common identity available in both event
// versions.
if _, exists := seenRegistrations[event.WalletPublicKeyHash]; exists {
return
}

for _, event := range legacyEvents {
convertedEvent := &tbtc.NewWalletRegisteredEvent{
WalletID: tbtc.DeriveLegacyWalletID(event.WalletPubKeyHash),
EcdsaWalletID: event.EcdsaWalletID,
WalletPublicKeyHash: event.WalletPubKeyHash,
BlockNumber: event.Raw.BlockNumber,
}
seenRegistrations[event.WalletPublicKeyHash] = struct{}{}
convertedEvents = append(convertedEvents, event)
}

// V2 events are appended first so they win over compatibility legacy
// events emitted for the same registration.
for _, event := range v2Events {
appendUnique(event)
}

for _, event := range legacyEvents {
// A genuine legacy ECDSA registration always carries a non-zero ECDSA
// wallet ID. A zero value can only be a compatibility event for a
// scheme whose canonical wallet ID is not present in this legacy event;
// synthesizing a padded-PKH ID would invent the wrong identity.
if event.EcdsaWalletID == [32]byte{} {
continue
}

convertedEvent := &tbtc.NewWalletRegisteredEvent{
WalletID: tbtc.DeriveLegacyWalletID(event.WalletPubKeyHash),
EcdsaWalletID: event.EcdsaWalletID,
WalletPublicKeyHash: event.WalletPubKeyHash,
BlockNumber: event.Raw.BlockNumber,
}

convertedEvents = append(convertedEvents, convertedEvent)
if len(walletID) > 0 && !containsWalletID(walletID, convertedEvent.WalletID) {
continue
}

appendUnique(convertedEvent)
}

sort.SliceStable(
Expand All @@ -1973,6 +2008,16 @@ func pastNewWalletRegisteredEvents(
return convertedEvents, nil
}

func containsWalletID(walletIDs [][32]byte, walletID [32]byte) bool {
for _, candidate := range walletIDs {
if candidate == walletID {
return true
}
}

return false
}

func pastNewWalletRegisteredV2Events(
startBlock uint64,
endBlock *uint64,
Expand Down
Loading
Loading