Skip to content

Commit

Permalink
chore: cleaning up btcstaking tooling library (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianElvis authored Nov 27, 2023
1 parent 857053e commit 98a5549
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 316 deletions.
29 changes: 21 additions & 8 deletions btcstaking/scripts.go → btcstaking/scripts_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ package btcstaking
import (
"bytes"
"fmt"
"sort"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript"
)

// private helper to build multisig script
// private helper to assemble multisig script
// SCRIPT: <Pk1> OP_CHEKCSIG <Pk2> OP_CHECKSIGADD <Pk3> OP_CHECKSIGADD ... <PkN> OP_CHECKSIGADD <threshold> OP_GREATERTHANOREQUAL OP_VERIFY
func buildMultiSigScript(
func assembleMultiSigScript(
pubkeys []*btcec.PublicKey,
threshold uint32,
withVerify bool,
Expand All @@ -36,6 +37,18 @@ func buildMultiSigScript(
return builder.Script()
}

// sortKeys takes a set of schnorr public keys and returns a new slice that is
// a copy of the keys sorted in lexicographical order bytes on the x-only
// pubkey serialization.
func sortKeys(keys []*btcec.PublicKey) []*btcec.PublicKey {
sort.SliceStable(keys, func(i, j int) bool {
keyIBytes := schnorr.SerializePubKey(keys[i])
keyJBytes := schnorr.SerializePubKey(keys[j])
return bytes.Compare(keyIBytes, keyJBytes) == -1
})
return keys
}

// prepareKeys prepares keys to be used in multisig script
// Validates:
// - whether there are at lest 2 keys
Expand All @@ -57,11 +70,11 @@ func prepareKeysForMultisigScript(keys []*btcec.PublicKey) ([]*btcec.PublicKey,
return sortedKeys, nil
}

// BuildMultiSigScript creates multisig script with given keys and signer threshold to
// buildMultiSigScript creates multisig script with given keys and signer threshold to
// successfully execute script
// it validates whether provided keys are unique and the threshold is not greater than number of keys
// If there is only one key provided it will return single key sig script
func BuildMultiSigScript(
func buildMultiSigScript(
keys []*btcec.PublicKey,
threshold uint32,
withVerify bool,
Expand All @@ -76,7 +89,7 @@ func BuildMultiSigScript(

if len(keys) == 1 {
// if we have only one key we can use single key sig script
return BuildSingleKeySigScript(keys[0], withVerify)
return buildSingleKeySigScript(keys[0], withVerify)
}

sortedKeys, err := prepareKeysForMultisigScript(keys)
Expand All @@ -85,12 +98,12 @@ func BuildMultiSigScript(
return nil, err
}

return buildMultiSigScript(sortedKeys, threshold, withVerify)
return assembleMultiSigScript(sortedKeys, threshold, withVerify)
}

// Only holder of private key for given pubKey can spend after relative lock time
// SCRIPT: <StakerPk> OP_CHECKSIGVERIFY <stakingTime> OP_CHECKSEQUENCEVERIFY
func BuildTimeLockScript(
func buildTimeLockScript(
pubKey *btcec.PublicKey,
lockTime uint16,
) ([]byte, error) {
Expand All @@ -104,7 +117,7 @@ func BuildTimeLockScript(

// Only holder of private key for given pubKey can spend
// SCRIPT: <pubKey> OP_CHECKSIGVERIFY
func BuildSingleKeySigScript(
func buildSingleKeySigScript(
pubKey *btcec.PublicKey,
withVerify bool,
) ([]byte, error) {
Expand Down
105 changes: 36 additions & 69 deletions btcstaking/staking.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package btcstaking

import (
"bytes"
"encoding/hex"
"fmt"

sdkmath "cosmossdk.io/math"
Expand All @@ -17,63 +16,6 @@ import (
asig "github.com/babylonchain/babylon/crypto/schnorr-adaptor-signature"
)

const (

// Point with unknown discrete logarithm defined in: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
// using it as internal public key efectively disables taproot key spends
unspendableKeyPath = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
)

var (
unspendableKeyPathKey = unspendableKeyPathInternalPubKeyInternal(unspendableKeyPath)
)

func unspendableKeyPathInternalPubKeyInternal(keyHex string) btcec.PublicKey {
keyBytes, err := hex.DecodeString(keyHex)

if err != nil {
panic(fmt.Sprintf("unexpected error: %v", err))
}

// We are using btcec here, as key is 33 byte compressed format.
pubKey, err := btcec.ParsePubKey(keyBytes)

if err != nil {
panic(fmt.Sprintf("unexpected error: %v", err))
}
return *pubKey
}

// StakingScriptData is a struct that holds data parsed from staking script
type StakingScriptData struct {
StakerKey *btcec.PublicKey
ValidatorKey *btcec.PublicKey
CovenantKey *btcec.PublicKey
StakingTime uint16
}

func NewStakingScriptData(
stakerKey,
validatorKey,
covenantKey *btcec.PublicKey,
stakingTime uint16) (*StakingScriptData, error) {

if stakerKey == nil || validatorKey == nil || covenantKey == nil {
return nil, fmt.Errorf("staker, validator and covenant keys cannot be nil")
}

return &StakingScriptData{
StakerKey: stakerKey,
ValidatorKey: validatorKey,
CovenantKey: covenantKey,
StakingTime: stakingTime,
}, nil
}

func UnspendableKeyPathInternalPubKey() btcec.PublicKey {
return unspendableKeyPathKey
}

// BuildSlashingTxFromOutpoint builds a valid slashing transaction by creating a new Bitcoin transaction that slashes a portion
// of staked funds and directs them to a specified slashing address. The transaction also includes a change output sent back to
// the specified change address. The slashing rate determines the proportion of staked funds to be slashed.
Expand Down Expand Up @@ -752,19 +694,44 @@ func EncVerifyTransactionSigWithOutputData(
return signature.EncVerify(pubKey, encKey, sigHash)
}

// IsSlashingRateValid checks if the given slashing rate is between the valid range i.e., (0,1) with a precision of at most 2 decimal places.
func IsSlashingRateValid(slashingRate sdkmath.LegacyDec) bool {
// Check if the slashing rate is between 0 and 1
if slashingRate.LTE(sdkmath.LegacyZeroDec()) || slashingRate.GTE(sdkmath.LegacyOneDec()) {
return false
// CreateBabylonWitness creates babylon compatible witness, as babylon scripts
// have witness with the same shape
// - first come signatures
// - then whole revealed script
// - then control block
func CreateBabylonWitness(
signatures [][]byte,
si *SpendInfo,
) (wire.TxWitness, error) {
numSignatures := len(signatures)

if numSignatures == 0 {
return nil, fmt.Errorf("cannot build witness without signatures")
}

if si == nil {
return nil, fmt.Errorf("cannot build witness without spend info")
}

controlBlockBytes, err := si.ControlBlock.ToBytes()

if err != nil {
return nil, err
}

// Multiply by 100 to move the decimal places and check if precision is at most 2 decimal places
multipliedRate := slashingRate.Mul(sdkmath.LegacyNewDec(100))
// witness stack has:
// all signatures
// whole revealed script
// control block
witnessStack := wire.TxWitness(make([][]byte, numSignatures+2))

for i, sig := range signatures {
sc := sig
witnessStack[i] = sc
}

// Truncate the rate to remove decimal places
truncatedRate := multipliedRate.TruncateDec()
witnessStack[numSignatures] = si.RevealedLeaf.Script
witnessStack[numSignatures+1] = controlBlockBytes

// Check if the truncated rate is equal to the original rate
return multipliedRate.Equal(truncatedRate)
return witnessStack, nil
}
34 changes: 31 additions & 3 deletions btcstaking/staking_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package btcstaking_test

import (
sdkmath "cosmossdk.io/math"
"fmt"
"math"
"math/rand"
"testing"

sdkmath "cosmossdk.io/math"

"github.com/babylonchain/babylon/btcstaking"
"github.com/babylonchain/babylon/testutil/datagen"
"github.com/btcsuite/btcd/btcec/v2"
Expand All @@ -16,7 +18,33 @@ import (
"github.com/stretchr/testify/require"
)

func genValidStakingScriptData(_ *testing.T, r *rand.Rand) *btcstaking.StakingScriptData {
// StakingScriptData is a struct that holds data parsed from staking script
type StakingScriptData struct {
StakerKey *btcec.PublicKey
ValidatorKey *btcec.PublicKey
CovenantKey *btcec.PublicKey
StakingTime uint16
}

func NewStakingScriptData(
stakerKey,
validatorKey,
covenantKey *btcec.PublicKey,
stakingTime uint16) (*StakingScriptData, error) {

if stakerKey == nil || validatorKey == nil || covenantKey == nil {
return nil, fmt.Errorf("staker, validator and covenant keys cannot be nil")
}

return &StakingScriptData{
StakerKey: stakerKey,
ValidatorKey: validatorKey,
CovenantKey: covenantKey,
StakingTime: stakingTime,
}, nil
}

func genValidStakingScriptData(_ *testing.T, r *rand.Rand) *StakingScriptData {
stakerPrivKeyBytes := datagen.GenRandomByteArray(r, 32)
validatorPrivKeyBytes := datagen.GenRandomByteArray(r, 32)
covenantPrivKeyBytes := datagen.GenRandomByteArray(r, 32)
Expand All @@ -26,7 +54,7 @@ func genValidStakingScriptData(_ *testing.T, r *rand.Rand) *btcstaking.StakingSc
_, validatorPublicKey := btcec.PrivKeyFromBytes(validatorPrivKeyBytes)
_, covenantPublicKey := btcec.PrivKeyFromBytes(covenantPrivKeyBytes)

sd, _ := btcstaking.NewStakingScriptData(stakerPublicKey, validatorPublicKey, covenantPublicKey, stakingTime)
sd, _ := NewStakingScriptData(stakerPublicKey, validatorPublicKey, covenantPublicKey, stakingTime)

return sd
}
Expand Down
Loading

0 comments on commit 98a5549

Please sign in to comment.