2023-08-23 15:36:21 +09:00

262 lines
8.0 KiB
Go

package libc4exwallet
import (
"github.com/c4ei/yunseokyeol/cmd/c4exwallet/libc4exwallet/bip32"
"github.com/c4ei/yunseokyeol/cmd/c4exwallet/libc4exwallet/serialization"
"github.com/c4ei/yunseokyeol/domain/consensus/model/externalapi"
"github.com/c4ei/yunseokyeol/domain/consensus/utils/constants"
"github.com/c4ei/yunseokyeol/domain/consensus/utils/subnetworks"
"github.com/c4ei/yunseokyeol/domain/consensus/utils/txscript"
"github.com/c4ei/yunseokyeol/util"
"github.com/pkg/errors"
)
// Payment contains a recipient payment details
type Payment struct {
Address util.Address
Amount uint64
}
// UTXO is a type that stores a UTXO and meta data
// that is needed in order to sign it and create
// transactions with it.
type UTXO struct {
Outpoint *externalapi.DomainOutpoint
UTXOEntry externalapi.UTXOEntry
DerivationPath string
}
// CreateUnsignedTransaction creates an unsigned transaction
func CreateUnsignedTransaction(
extendedPublicKeys []string,
minimumSignatures uint32,
payments []*Payment,
selectedUTXOs []*UTXO) ([]byte, error) {
sortPublicKeys(extendedPublicKeys)
unsignedTransaction, err := createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
if err != nil {
return nil, err
}
return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
}
func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) {
scriptBuilder := txscript.NewScriptBuilder()
scriptBuilder.AddInt64(int64(minimumSignatures))
for _, key := range extendedPublicKeys {
extendedKey, err := bip32.DeserializeExtendedKey(key)
if err != nil {
return nil, err
}
derivedKey, err := extendedKey.DeriveFromPath(path)
if err != nil {
return nil, err
}
publicKey, err := derivedKey.PublicKey()
if err != nil {
return nil, err
}
var serializedPublicKey []byte
if ecdsa {
serializedECDSAPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, err
}
serializedPublicKey = serializedECDSAPublicKey[:]
} else {
schnorrPublicKey, err := publicKey.ToSchnorr()
if err != nil {
return nil, err
}
serializedSchnorrPublicKey, err := schnorrPublicKey.Serialize()
if err != nil {
return nil, err
}
serializedPublicKey = serializedSchnorrPublicKey[:]
}
scriptBuilder.AddData(serializedPublicKey)
}
scriptBuilder.AddInt64(int64(len(extendedPublicKeys)))
if ecdsa {
scriptBuilder.AddOp(txscript.OpCheckMultiSigECDSA)
} else {
scriptBuilder.AddOp(txscript.OpCheckMultiSig)
}
return scriptBuilder.Script()
}
func createUnsignedTransaction(
extendedPublicKeys []string,
minimumSignatures uint32,
payments []*Payment,
selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
inputs := make([]*externalapi.DomainTransactionInput, len(selectedUTXOs))
partiallySignedInputs := make([]*serialization.PartiallySignedInput, len(selectedUTXOs))
for i, utxo := range selectedUTXOs {
emptyPubKeySignaturePairs := make([]*serialization.PubKeySignaturePair, len(extendedPublicKeys))
for i, extendedPublicKey := range extendedPublicKeys {
extendedKey, err := bip32.DeserializeExtendedKey(extendedPublicKey)
if err != nil {
return nil, err
}
derivedKey, err := extendedKey.DeriveFromPath(utxo.DerivationPath)
if err != nil {
return nil, err
}
emptyPubKeySignaturePairs[i] = &serialization.PubKeySignaturePair{
ExtendedPublicKey: derivedKey.String(),
}
}
inputs[i] = &externalapi.DomainTransactionInput{PreviousOutpoint: *utxo.Outpoint}
partiallySignedInputs[i] = &serialization.PartiallySignedInput{
PrevOutput: &externalapi.DomainTransactionOutput{
Value: utxo.UTXOEntry.Amount(),
ScriptPublicKey: utxo.UTXOEntry.ScriptPublicKey(),
},
MinimumSignatures: minimumSignatures,
PubKeySignaturePairs: emptyPubKeySignaturePairs,
DerivationPath: utxo.DerivationPath,
}
}
outputs := make([]*externalapi.DomainTransactionOutput, len(payments))
for i, payment := range payments {
scriptPublicKey, err := txscript.PayToAddrScript(payment.Address)
if err != nil {
return nil, err
}
outputs[i] = &externalapi.DomainTransactionOutput{
Value: payment.Amount,
ScriptPublicKey: scriptPublicKey,
}
}
domainTransaction := &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: inputs,
Outputs: outputs,
LockTime: 0,
SubnetworkID: subnetworks.SubnetworkIDNative,
Gas: 0,
Payload: nil,
}
return &serialization.PartiallySignedTransaction{
Tx: domainTransaction,
PartiallySignedInputs: partiallySignedInputs,
}, nil
}
// IsTransactionFullySigned returns whether the transaction is fully signed and ready to broadcast.
func IsTransactionFullySigned(partiallySignedTransactionBytes []byte) (bool, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(partiallySignedTransactionBytes)
if err != nil {
return false, err
}
return isTransactionFullySigned(partiallySignedTransaction), nil
}
func isTransactionFullySigned(partiallySignedTransaction *serialization.PartiallySignedTransaction) bool {
for _, input := range partiallySignedTransaction.PartiallySignedInputs {
numSignatures := 0
for _, pair := range input.PubKeySignaturePairs {
if pair.Signature != nil {
numSignatures++
}
}
if uint32(numSignatures) < input.MinimumSignatures {
return false
}
}
return true
}
// ExtractTransaction extracts a domain transaction from partially signed transaction after all of the
// relevant parties have signed it.
func ExtractTransaction(partiallySignedTransactionBytes []byte, ecdsa bool) (*externalapi.DomainTransaction, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(partiallySignedTransactionBytes)
if err != nil {
return nil, err
}
return ExtractTransactionDeserialized(partiallySignedTransaction, ecdsa)
}
// ExtractTransactionDeserialized does the same thing ExtractTransaction does, only receives the PartiallySignedTransaction
// in an already deserialized format
func ExtractTransactionDeserialized(partiallySignedTransaction *serialization.PartiallySignedTransaction, ecdsa bool) (
*externalapi.DomainTransaction, error) {
for i, input := range partiallySignedTransaction.PartiallySignedInputs {
isMultisig := len(input.PubKeySignaturePairs) > 1
scriptBuilder := txscript.NewScriptBuilder()
if isMultisig {
signatureCount := 0
for _, pair := range input.PubKeySignaturePairs {
if pair.Signature != nil {
scriptBuilder.AddData(pair.Signature)
signatureCount++
}
}
if uint32(signatureCount) < input.MinimumSignatures {
return nil, errors.Errorf("missing %d signatures", input.MinimumSignatures-uint32(signatureCount))
}
redeemScript, err := partiallySignedInputMultisigRedeemScript(input, ecdsa)
if err != nil {
return nil, err
}
scriptBuilder.AddData(redeemScript)
sigScript, err := scriptBuilder.Script()
if err != nil {
return nil, err
}
partiallySignedTransaction.Tx.Inputs[i].SignatureScript = sigScript
} else {
if len(input.PubKeySignaturePairs) > 1 {
return nil, errors.Errorf("Cannot sign on P2PK when len(input.PubKeySignaturePairs) > 1")
}
if input.PubKeySignaturePairs[0].Signature == nil {
return nil, errors.Errorf("missing signature")
}
sigScript, err := txscript.NewScriptBuilder().
AddData(input.PubKeySignaturePairs[0].Signature).
Script()
if err != nil {
return nil, err
}
partiallySignedTransaction.Tx.Inputs[i].SignatureScript = sigScript
}
}
return partiallySignedTransaction.Tx, nil
}
func partiallySignedInputMultisigRedeemScript(input *serialization.PartiallySignedInput, ecdsa bool) ([]byte, error) {
extendedPublicKeys := make([]string, len(input.PubKeySignaturePairs))
for i, pair := range input.PubKeySignaturePairs {
extendedPublicKeys[i] = pair.ExtendedPublicKey
}
return multiSigRedeemScript(extendedPublicKeys, input.MinimumSignatures, "m", ecdsa)
}