diff --git a/src/shared/Angor.Shared/Models/TransactionInfo.cs b/src/shared/Angor.Shared/Models/TransactionInfo.cs index fc542de51..588c99ed3 100644 --- a/src/shared/Angor.Shared/Models/TransactionInfo.cs +++ b/src/shared/Angor.Shared/Models/TransactionInfo.cs @@ -6,5 +6,16 @@ public class TransactionInfo { public Transaction Transaction { get; set; } public long TransactionFee { get; set; } - + + /// + /// Network-specific transaction hex. On Bitcoin this is the same as Transaction.ToHex(). + /// On Liquid, this holds the Elements-format transaction hex. + /// + public string? TransactionHex { get; set; } + + /// + /// Network-specific transaction ID. On Bitcoin this is the same as Transaction.GetHash(). + /// On Liquid, this holds the Elements transaction hash. + /// + public string? TransactionId { get; set; } } \ No newline at end of file diff --git a/src/shared/Angor.Shared/Protocol/BitcoinTransactionTransformer.cs b/src/shared/Angor.Shared/Protocol/BitcoinTransactionTransformer.cs new file mode 100644 index 000000000..a1f75c100 --- /dev/null +++ b/src/shared/Angor.Shared/Protocol/BitcoinTransactionTransformer.cs @@ -0,0 +1,57 @@ +using Angor.Shared.Models; +using NBitcoin; +using Transaction = NBitcoin.Transaction; + +namespace Angor.Shared.Protocol; + +/// +/// No-op transaction transformer for Bitcoin networks. +/// All methods return their inputs unchanged since no transformation is needed. +/// +public class BitcoinTransactionTransformer : ITransactionTransformer +{ + public TransactionInfo WrapP2WPKH( + TransactionInfo bitcoinTxInfo, + List coins, + List keys) + { + return bitcoinTxInfo; + } + + public TransactionInfo WrapTaproot( + TransactionInfo bitcoinTxInfo, + Key nbitcoinKey, + TxOut[] spentOutputs) + { + return bitcoinTxInfo; + } + + public SignatureInfo WrapTaprootSignatures( + SignatureInfo signatureInfo, + Transaction recoveryTx, + TxOut[] spentOutputs, + Key nbitcoinKey, + Func scriptPerStage) + { + return signatureInfo; + } + + public TransactionInfo WrapP2WSH( + TransactionInfo bitcoinTxInfo, + Key nbitcoinKey, + Script spendingScript, + Transaction recoveryTransaction) + { + return bitcoinTxInfo; + } + + public Transaction GetSighashTransaction(Transaction bitcoinTx) + { + return bitcoinTx; + } + + public TxOut[] GetSighashSpentOutputs(TxOut[] spentOutputs) + { + return spentOutputs; + } +} diff --git a/src/shared/Angor.Shared/Protocol/ITransactionTransformer.cs b/src/shared/Angor.Shared/Protocol/ITransactionTransformer.cs new file mode 100644 index 000000000..4211a5ec7 --- /dev/null +++ b/src/shared/Angor.Shared/Protocol/ITransactionTransformer.cs @@ -0,0 +1,62 @@ +using Angor.Shared.Models; +using NBitcoin; +using Transaction = NBitcoin.Transaction; + +namespace Angor.Shared.Protocol; + +/// +/// Transforms transactions for the target network (Bitcoin or Liquid). +/// On Bitcoin, all methods are no-ops returning their inputs unchanged. +/// On Liquid, methods rebuild transactions as Elements format with explicit L-BTC asset tags. +/// +public interface ITransactionTransformer +{ + /// + /// Wraps a fully-signed P2WPKH transaction for the target network. + /// + TransactionInfo WrapP2WPKH( + TransactionInfo bitcoinTxInfo, + List coins, + List keys); + + /// + /// Wraps a fully-signed Taproot script-path transaction for the target network. + /// + TransactionInfo WrapTaproot( + TransactionInfo bitcoinTxInfo, + Key nbitcoinKey, + TxOut[] spentOutputs); + + /// + /// Wraps Taproot signature strings for the target network. + /// Re-computes sighash on the network-appropriate version of the recovery transaction + /// and re-signs each stage. + /// + SignatureInfo WrapTaprootSignatures( + SignatureInfo signatureInfo, + Transaction recoveryTx, + TxOut[] spentOutputs, + Key nbitcoinKey, + Func scriptPerStage); + + /// + /// Wraps a fully-signed P2WSH transaction for the target network. + /// + TransactionInfo WrapP2WSH( + TransactionInfo bitcoinTxInfo, + Key nbitcoinKey, + Script spendingScript, + Transaction recoveryTransaction); + + /// + /// Returns the transaction to use for sighash computation. + /// On Bitcoin, returns the input unchanged. On Liquid, returns an Elements transaction. + /// + Transaction GetSighashTransaction(Transaction bitcoinTx); + + /// + /// Returns the spent outputs to use for Taproot sighash computation. + /// On Bitcoin, returns the input unchanged. On Liquid, returns Elements-format outputs. + /// + TxOut[] GetSighashSpentOutputs(TxOut[] spentOutputs); +} diff --git a/src/shared/Angor.Shared/Protocol/LiquidTransactionTransformer.cs b/src/shared/Angor.Shared/Protocol/LiquidTransactionTransformer.cs new file mode 100644 index 000000000..1691bd6b3 --- /dev/null +++ b/src/shared/Angor.Shared/Protocol/LiquidTransactionTransformer.cs @@ -0,0 +1,228 @@ +using NBitcoin; +using NBitcoin.Altcoins; +using NBitcoin.Altcoins.Elements; +using Angor.Shared.Models; + +namespace Angor.Shared.Protocol; + +/// +/// Transaction transformer for Liquid (Elements) networks. +/// Wraps fully-signed Bitcoin transactions and re-creates them as valid Liquid transactions +/// with explicit L-BTC asset tags and an explicit fee output, then re-signs. +/// +public class LiquidTransactionTransformer : ITransactionTransformer +{ + private static readonly uint256 LBtcAssetId = ElementsParams.PeggedAssetId; + private static readonly Network LiquidNetwork = Liquid.Instance.Mainnet; + + private readonly Blockcore.Networks.Network blockcoreNetwork; + + public LiquidTransactionTransformer(Blockcore.Networks.Network blockcoreNetwork) + { + this.blockcoreNetwork = blockcoreNetwork; + } + + public TransactionInfo WrapP2WPKH( + TransactionInfo bitcoinTxInfo, + List coins, + List keys) + { + var fee = bitcoinTxInfo.TransactionFee; + var bitcoinTx = Transaction.Parse(bitcoinTxInfo.Transaction.ToHex(), Network.Main); + var elementsTx = BuildElementsTransaction(bitcoinTx, Money.Satoshis(fee)); + + var builder = LiquidNetwork.CreateTransactionBuilder(); + + foreach (var coin in coins) + { + var elemCoin = new Coin( + new OutPoint(new uint256(coin.Outpoint.Hash.ToString()), coin.Outpoint.N), + CreateExplicitOutput( + Money.Satoshis(coin.Amount.Satoshi), + new Script(coin.ScriptPubKey.ToBytes()))); + builder.AddCoins(elemCoin); + } + + foreach (var blockcoreKey in keys) + { + builder.AddKeys(BlockcoreKeyToNBitcoinKey(blockcoreKey)); + } + + builder.SignTransactionInPlace(elementsTx); + + return CreateTransactionInfo(elementsTx, fee, bitcoinTxInfo); + } + + public TransactionInfo WrapTaproot( + TransactionInfo bitcoinTxInfo, + Key nbitcoinKey, + TxOut[] spentOutputs) + { + var fee = bitcoinTxInfo.TransactionFee; + var bitcoinTx = Transaction.Parse(bitcoinTxInfo.Transaction.ToHex(), Network.Main); + var elementsTx = BuildElementsTransaction(bitcoinTx, Money.Satoshis(fee)); + var elemSpentOutputs = TransformSpentOutputs(spentOutputs); + var trxData = elementsTx.PrecomputeTransactionData(elemSpentOutputs); + + const TaprootSigHash sigHash = TaprootSigHash.All; + + for (int i = 0; i < elementsTx.Inputs.Count; i++) + { + var bitcoinWit = bitcoinTx.Inputs[i].WitScript; + var scriptToExecute = new Script(bitcoinWit[bitcoinWit.PushCount - 2]); + var controlBlockBytes = bitcoinWit[bitcoinWit.PushCount - 1]; + + var leafHash = scriptToExecute.ToTapScript(TapLeafVersion.C0).LeafHash; + var hash = elementsTx.GetSignatureHashTaproot(trxData, + new TaprootExecutionData(i, leafHash) { SigHash = sigHash }); + + var sig = nbitcoinKey.SignTaprootKeySpend(hash, sigHash); + + var witOps = new List(); + witOps.Add(sig.ToBytes()); + for (int p = 1; p < bitcoinWit.PushCount - 2; p++) + { + witOps.Add(bitcoinWit[p]); + } + witOps.Add(scriptToExecute.ToBytes()); + witOps.Add(controlBlockBytes); + + elementsTx.Inputs[i].WitScript = new WitScript( + witOps.Select(Op.GetPushOp).ToArray()); + } + + return CreateTransactionInfo(elementsTx, fee, bitcoinTxInfo); + } + + public SignatureInfo WrapTaprootSignatures( + SignatureInfo signatureInfo, + Transaction recoveryTx, + TxOut[] spentOutputs, + Key nbitcoinKey, + Func scriptPerStage) + { + var elementsTx = BuildElementsTransaction(recoveryTx, Money.Zero); + var elemSpentOutputs = TransformSpentOutputs(spentOutputs); + + const TaprootSigHash sigHash = TaprootSigHash.Single | TaprootSigHash.AnyoneCanPay; + + foreach (var sigItem in signatureInfo.Signatures) + { + var tapScript = scriptPerStage(sigItem.StageIndex).ToTapScript(TapLeafVersion.C0); + var execData = new TaprootExecutionData(sigItem.StageIndex, tapScript.LeafHash) { SigHash = sigHash }; + var hash = elementsTx.GetSignatureHashTaproot(elemSpentOutputs, execData); + + var sig = nbitcoinKey.SignTaprootKeySpend(hash, sigHash); + sigItem.Signature = sig.ToString(); + } + + return signatureInfo; + } + + public TransactionInfo WrapP2WSH( + TransactionInfo bitcoinTxInfo, + Key nbitcoinKey, + Script spendingScript, + Transaction recoveryTransaction) + { + var fee = bitcoinTxInfo.TransactionFee; + var bitcoinTx = Transaction.Parse(bitcoinTxInfo.Transaction.ToHex(), Network.Main); + var elementsTx = BuildElementsTransaction(bitcoinTx, Money.Satoshis(fee)); + + var builder = LiquidNetwork.CreateTransactionBuilder(); + + foreach (var input in elementsTx.Inputs) + { + var spendingOutput = recoveryTransaction.Outputs.AsIndexedOutputs() + .First(f => f.N == input.PrevOut.N); + + var elemOutput = CreateExplicitOutput(spendingOutput.TxOut.Value, spendingOutput.TxOut.ScriptPubKey); + var coin = new Coin(input.PrevOut, elemOutput); + var scriptCoin = coin.ToScriptCoin(spendingScript); + builder.AddCoins(scriptCoin); + } + + builder.AddKeys(nbitcoinKey); + builder.SignTransactionInPlace(elementsTx); + + return CreateTransactionInfo(elementsTx, fee, bitcoinTxInfo); + } + + public Transaction GetSighashTransaction(Transaction bitcoinTx) + { + return BuildElementsTransaction(bitcoinTx, Money.Zero); + } + + public TxOut[] GetSighashSpentOutputs(TxOut[] spentOutputs) + { + return TransformSpentOutputs(spentOutputs); + } + + private Transaction BuildElementsTransaction(Transaction bitcoinTx, Money fee) + { + var elementsTx = LiquidNetwork.CreateTransaction(); + + elementsTx.Version = bitcoinTx.Version; + elementsTx.LockTime = bitcoinTx.LockTime; + + foreach (var input in bitcoinTx.Inputs) + { + var elemInput = (ElementsTxIn)LiquidNetwork.Consensus.ConsensusFactory.CreateTxIn(); + elemInput.PrevOut = input.PrevOut; + elemInput.Sequence = input.Sequence; + if (input.ScriptSig != null && input.ScriptSig != Script.Empty) + { + elemInput.ScriptSig = input.ScriptSig; + } + + elementsTx.Inputs.Add(elemInput); + } + + foreach (var output in bitcoinTx.Outputs) + { + elementsTx.Outputs.Add(CreateExplicitOutput(output.Value, output.ScriptPubKey)); + } + + if (fee > Money.Zero) + { + elementsTx.Outputs.Add(CreateExplicitOutput(fee, Script.Empty)); + } + + return elementsTx; + } + + private static ElementsTxOut CreateExplicitOutput(Money amount, Script scriptPubKey) + { + var elemOutput = new ElementsTxOut(); + elemOutput.Value = amount; + elemOutput.ScriptPubKey = scriptPubKey; + elemOutput.Asset = new ConfidentialAsset(LBtcAssetId); + elemOutput.Nonce = new ConfidentialNonce(); + return elemOutput; + } + + private static TxOut[] TransformSpentOutputs(TxOut[] spentOutputs) + { + return spentOutputs.Select(o => (TxOut)CreateExplicitOutput(o.Value, o.ScriptPubKey)).ToArray(); + } + + private Key BlockcoreKeyToNBitcoinKey(Blockcore.NBitcoin.Key blockcoreKey) + { + var wif = blockcoreKey.GetBitcoinSecret(blockcoreNetwork).ToString(); + return Key.Parse(wif, LiquidNetwork); + } + + private static TransactionInfo CreateTransactionInfo( + Transaction signedElementsTx, + long fee, + TransactionInfo originalBitcoinTxInfo) + { + return new TransactionInfo + { + Transaction = originalBitcoinTxInfo.Transaction, + TransactionHex = signedElementsTx.ToHex(), + TransactionId = signedElementsTx.GetHash().ToString(), + TransactionFee = fee + }; + } +}