-
Notifications
You must be signed in to change notification settings - Fork 115
/
Copy pathsigner.go
495 lines (439 loc) · 14.3 KB
/
signer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
// Package signer implements the ChainSigner interface for BTC
package signer
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"math/big"
"time"
"github.com/btcsuite/btcd/btcec/v2"
btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/rpcclient"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/pkg/errors"
"github.com/zeta-chain/node/pkg/chains"
"github.com/zeta-chain/node/pkg/coin"
"github.com/zeta-chain/node/pkg/constant"
"github.com/zeta-chain/node/x/crosschain/types"
observertypes "github.com/zeta-chain/node/x/observer/types"
"github.com/zeta-chain/node/zetaclient/chains/base"
"github.com/zeta-chain/node/zetaclient/chains/bitcoin"
"github.com/zeta-chain/node/zetaclient/chains/bitcoin/observer"
"github.com/zeta-chain/node/zetaclient/chains/interfaces"
"github.com/zeta-chain/node/zetaclient/compliance"
"github.com/zeta-chain/node/zetaclient/config"
"github.com/zeta-chain/node/zetaclient/logs"
"github.com/zeta-chain/node/zetaclient/outboundprocessor"
)
const (
// the maximum number of inputs per outbound
MaxNoOfInputsPerTx = 20
// the rank below (or equal to) which we consolidate UTXOs
consolidationRank = 10
// broadcastBackoff is the initial backoff duration for retrying broadcast
broadcastBackoff = 1000 * time.Millisecond
// broadcastRetries is the maximum number of retries for broadcasting a transaction
broadcastRetries = 5
)
var _ interfaces.ChainSigner = (*Signer)(nil)
// Signer deals with signing BTC transactions and implements the ChainSigner interface
type Signer struct {
*base.Signer
// client is the RPC client to interact with the Bitcoin chain
client interfaces.BTCRPCClient
}
// NewSigner creates a new Bitcoin signer
func NewSigner(
chain chains.Chain,
tss interfaces.TSSSigner,
logger base.Logger,
cfg config.BTCConfig,
) (*Signer, error) {
// create base signer
baseSigner := base.NewSigner(chain, tss, logger)
// create the bitcoin rpc client using the provided config
connCfg := &rpcclient.ConnConfig{
Host: cfg.RPCHost,
User: cfg.RPCUsername,
Pass: cfg.RPCPassword,
HTTPPostMode: true,
DisableTLS: true,
Params: cfg.RPCParams,
}
client, err := rpcclient.New(connCfg, nil)
if err != nil {
return nil, errors.Wrap(err, "unable to create bitcoin rpc client")
}
return &Signer{
Signer: baseSigner,
client: client,
}, nil
}
// TODO: get rid of below four get/set functions for Bitcoin, as they are not needed in future
// https://github.com/zeta-chain/node/issues/2532
// SetZetaConnectorAddress does nothing for BTC
func (signer *Signer) SetZetaConnectorAddress(_ ethcommon.Address) {
}
// SetERC20CustodyAddress does nothing for BTC
func (signer *Signer) SetERC20CustodyAddress(_ ethcommon.Address) {
}
// GetZetaConnectorAddress returns dummy address
func (signer *Signer) GetZetaConnectorAddress() ethcommon.Address {
return ethcommon.Address{}
}
// GetERC20CustodyAddress returns dummy address
func (signer *Signer) GetERC20CustodyAddress() ethcommon.Address {
return ethcommon.Address{}
}
// SetGatewayAddress does nothing for BTC
// Note: TSS address will be used as gateway address for Bitcoin
func (signer *Signer) SetGatewayAddress(_ string) {
}
// GetGatewayAddress returns empty address
// Note: same as SetGatewayAddress
func (signer *Signer) GetGatewayAddress() string {
return ""
}
// AddWithdrawTxOutputs adds the 3 outputs to the withdraw tx
// 1st output: the nonce-mark btc to TSS itself
// 2nd output: the payment to the recipient
// 3rd output: the remaining btc to TSS itself
func (signer *Signer) AddWithdrawTxOutputs(
tx *wire.MsgTx,
to btcutil.Address,
total float64,
amount float64,
nonceMark int64,
fees *big.Int,
cancelTx bool,
) error {
// convert withdraw amount to satoshis
amountSatoshis, err := bitcoin.GetSatoshis(amount)
if err != nil {
return err
}
// calculate remaining btc (the change) to TSS self
remaining := total - amount
remainingSats, err := bitcoin.GetSatoshis(remaining)
if err != nil {
return err
}
remainingSats -= fees.Int64()
remainingSats -= nonceMark
if remainingSats < 0 {
return fmt.Errorf("remainder value is negative: %d", remainingSats)
} else if remainingSats == nonceMark {
signer.Logger().Std.Info().Msgf("adjust remainder value to avoid duplicate nonce-mark: %d", remainingSats)
remainingSats--
}
// 1st output: the nonce-mark btc to TSS self
tssAddrP2WPKH, err := signer.TSS().PubKey().AddressBTC(signer.Chain().ChainId)
if err != nil {
return err
}
payToSelfScript, err := txscript.PayToAddrScript(tssAddrP2WPKH)
if err != nil {
return err
}
txOut1 := wire.NewTxOut(nonceMark, payToSelfScript)
tx.AddTxOut(txOut1)
// 2nd output: the payment to the recipient
if !cancelTx {
pkScript, err := txscript.PayToAddrScript(to)
if err != nil {
return err
}
txOut2 := wire.NewTxOut(amountSatoshis, pkScript)
tx.AddTxOut(txOut2)
} else {
// send the amount to TSS self if tx is cancelled
remainingSats += amountSatoshis
}
// 3rd output: the remaining btc to TSS self
if remainingSats > 0 {
txOut3 := wire.NewTxOut(remainingSats, payToSelfScript)
tx.AddTxOut(txOut3)
}
return nil
}
// SignWithdrawTx receives utxos sorted by value, amount in BTC, feeRate in BTC per Kb
// TODO(revamp): simplify the function
func (signer *Signer) SignWithdrawTx(
ctx context.Context,
to btcutil.Address,
amount float64,
gasPrice *big.Int,
sizeLimit uint64,
observer *observer.Observer,
height uint64,
nonce uint64,
chain chains.Chain,
cancelTx bool,
) (*wire.MsgTx, error) {
estimateFee := float64(gasPrice.Uint64()*bitcoin.OutboundBytesMax) / 1e8
nonceMark := chains.NonceMarkAmount(nonce)
// refresh unspent UTXOs and continue with keysign regardless of error
err := observer.FetchUTXOs(ctx)
if err != nil {
signer.Logger().
Std.Error().
Err(err).
Msgf("SignGasWithdraw: FetchUTXOs error: nonce %d chain %d", nonce, chain.ChainId)
}
// select N UTXOs to cover the total expense
prevOuts, total, consolidatedUtxo, consolidatedValue, err := observer.SelectUTXOs(
ctx,
amount+estimateFee+float64(nonceMark)*1e-8,
MaxNoOfInputsPerTx,
nonce,
consolidationRank,
false,
)
if err != nil {
return nil, err
}
// build tx with selected unspents
tx := wire.NewMsgTx(wire.TxVersion)
for _, prevOut := range prevOuts {
hash, err := chainhash.NewHashFromStr(prevOut.TxID)
if err != nil {
return nil, err
}
outpoint := wire.NewOutPoint(hash, prevOut.Vout)
txIn := wire.NewTxIn(outpoint, nil, nil)
tx.AddTxIn(txIn)
}
// size checking
// #nosec G115 always positive
txSize, err := bitcoin.EstimateOutboundSize(uint64(len(prevOuts)), []btcutil.Address{to})
if err != nil {
return nil, err
}
if sizeLimit < bitcoin.BtcOutboundBytesWithdrawer { // ZRC20 'withdraw' charged less fee from end user
signer.Logger().Std.Info().
Msgf("sizeLimit %d is less than BtcOutboundBytesWithdrawer %d for nonce %d", sizeLimit, txSize, nonce)
}
if txSize < bitcoin.OutboundBytesMin { // outbound shouldn't be blocked a low sizeLimit
signer.Logger().Std.Warn().
Msgf("txSize %d is less than outboundBytesMin %d; use outboundBytesMin", txSize, bitcoin.OutboundBytesMin)
txSize = bitcoin.OutboundBytesMin
}
if txSize > bitcoin.OutboundBytesMax { // in case of accident
signer.Logger().Std.Warn().
Msgf("txSize %d is greater than outboundBytesMax %d; use outboundBytesMax", txSize, bitcoin.OutboundBytesMax)
txSize = bitcoin.OutboundBytesMax
}
// fee calculation
// #nosec G115 always in range (checked above)
fees := new(big.Int).Mul(big.NewInt(int64(txSize)), gasPrice)
signer.Logger().
Std.Info().
Msgf("bitcoin outbound nonce %d gasPrice %s size %d fees %s consolidated %d utxos of value %v",
nonce, gasPrice.String(), txSize, fees.String(), consolidatedUtxo, consolidatedValue)
// add tx outputs
err = signer.AddWithdrawTxOutputs(tx, to, total, amount, nonceMark, fees, cancelTx)
if err != nil {
return nil, err
}
// sign the tx
sigHashes := txscript.NewTxSigHashes(tx, txscript.NewCannedPrevOutputFetcher([]byte{}, 0))
witnessHashes := make([][]byte, len(tx.TxIn))
for ix := range tx.TxIn {
amt, err := bitcoin.GetSatoshis(prevOuts[ix].Amount)
if err != nil {
return nil, err
}
pkScript, err := hex.DecodeString(prevOuts[ix].ScriptPubKey)
if err != nil {
return nil, err
}
witnessHashes[ix], err = txscript.CalcWitnessSigHash(pkScript, sigHashes, txscript.SigHashAll, tx, ix, amt)
if err != nil {
return nil, err
}
}
sig65Bs, err := signer.TSS().SignBatch(ctx, witnessHashes, height, nonce, chain.ChainId)
if err != nil {
return nil, fmt.Errorf("SignBatch error: %v", err)
}
for ix := range tx.TxIn {
sig65B := sig65Bs[ix]
R := &btcec.ModNScalar{}
R.SetBytes((*[32]byte)(sig65B[:32]))
S := &btcec.ModNScalar{}
S.SetBytes((*[32]byte)(sig65B[32:64]))
sig := btcecdsa.NewSignature(R, S)
pkCompressed := signer.TSS().PubKey().Bytes(true)
hashType := txscript.SigHashAll
txWitness := wire.TxWitness{append(sig.Serialize(), byte(hashType)), pkCompressed}
tx.TxIn[ix].Witness = txWitness
}
return tx, nil
}
// Broadcast sends the signed transaction to the network
func (signer *Signer) Broadcast(signedTx *wire.MsgTx) error {
fmt.Printf("BTCSigner: Broadcasting: %s\n", signedTx.TxHash().String())
var outBuff bytes.Buffer
err := signedTx.Serialize(&outBuff)
if err != nil {
return err
}
str := hex.EncodeToString(outBuff.Bytes())
fmt.Printf("BTCSigner: Transaction Data: %s\n", str)
hash, err := signer.client.SendRawTransaction(signedTx, true)
if err != nil {
return err
}
signer.Logger().Std.Info().Msgf("Broadcasting BTC tx , hash %s ", hash)
return nil
}
// TryProcessOutbound signs and broadcasts a BTC transaction from a new outbound
// TODO(revamp): simplify the function
func (signer *Signer) TryProcessOutbound(
ctx context.Context,
cctx *types.CrossChainTx,
outboundProcessor *outboundprocessor.Processor,
outboundID string,
chainObserver interfaces.ChainObserver,
zetacoreClient interfaces.ZetacoreClient,
height uint64,
) {
// end outbound process on panic
defer func() {
outboundProcessor.EndTryProcess(outboundID)
if err := recover(); err != nil {
signer.Logger().Std.Error().Msgf("BTC TryProcessOutbound: %s, caught panic error: %v", cctx.Index, err)
}
}()
// prepare logger
params := cctx.GetCurrentOutboundParam()
// prepare logger fields
lf := map[string]any{
logs.FieldMethod: "TryProcessOutbound",
logs.FieldCctx: cctx.Index,
logs.FieldNonce: params.TssNonce,
}
logger := signer.Logger().Std.With().Fields(lf).Logger()
// support gas token only for Bitcoin outbound
coinType := cctx.InboundParams.CoinType
if coinType == coin.CoinType_Zeta || coinType == coin.CoinType_ERC20 {
logger.Error().Msg("can only send BTC to a BTC network")
return
}
// convert chain observer to BTC observer
btcObserver, ok := chainObserver.(*observer.Observer)
if !ok {
logger.Error().Msg("chain observer is not a bitcoin observer")
return
}
chain := btcObserver.Chain()
outboundTssNonce := params.TssNonce
signerAddress, err := zetacoreClient.GetKeys().GetAddress()
if err != nil {
logger.Error().Err(err).Msg("cannot get signer address")
return
}
lf["signer"] = signerAddress.String()
// get size limit and gas price
sizelimit := params.CallOptions.GasLimit
gasprice, ok := new(big.Int).SetString(params.GasPrice, 10)
if !ok || gasprice.Cmp(big.NewInt(0)) < 0 {
logger.Error().Msgf("cannot convert gas price %s ", params.GasPrice)
return
}
// Check receiver P2WPKH address
to, err := chains.DecodeBtcAddress(params.Receiver, params.ReceiverChainId)
if err != nil {
logger.Error().Err(err).Msgf("cannot decode address %s ", params.Receiver)
return
}
if !chains.IsBtcAddressSupported(to) {
logger.Error().Msgf("unsupported address %s", params.Receiver)
return
}
amount := float64(params.Amount.Uint64()) / 1e8
// Add 1 satoshi/byte to gasPrice to avoid minRelayTxFee issue
networkInfo, err := signer.client.GetNetworkInfo()
if err != nil {
logger.Error().Err(err).Msgf("cannot get bitcoin network info")
return
}
satPerByte := bitcoin.FeeRateToSatPerByte(networkInfo.RelayFee)
gasprice.Add(gasprice, satPerByte)
// compliance check
restrictedCCTX := compliance.IsCctxRestricted(cctx)
if restrictedCCTX {
compliance.PrintComplianceLog(logger, signer.Logger().Compliance,
true, chain.ChainId, cctx.Index, cctx.InboundParams.Sender, params.Receiver, "BTC")
}
// check dust amount
dustAmount := params.Amount.Uint64() < constant.BTCWithdrawalDustAmount
if dustAmount {
logger.Warn().Msgf("dust amount %d sats, canceling tx", params.Amount.Uint64())
}
// set the amount to 0 when the tx should be cancelled
cancelTx := restrictedCCTX || dustAmount
if cancelTx {
amount = 0.0
}
// sign withdraw tx
tx, err := signer.SignWithdrawTx(
ctx,
to,
amount,
gasprice,
sizelimit,
btcObserver,
height,
outboundTssNonce,
chain,
cancelTx,
)
if err != nil {
logger.Warn().Err(err).Msg("SignWithdrawTx failed")
return
}
logger.Info().Msg("Key-sign success")
// FIXME: add prometheus metrics
_, err = zetacoreClient.GetObserverList(ctx)
if err != nil {
logger.Warn().
Err(err).Stringer("observation_type", observertypes.ObservationType_OutboundTx).
Msg("unable to get observer list, observation")
}
if tx != nil {
outboundHash := tx.TxHash().String()
lf[logs.FieldTx] = outboundHash
// try broacasting tx with increasing backoff (1s, 2s, 4s, 8s, 16s) in case of RPC error
backOff := broadcastBackoff
for i := 0; i < broadcastRetries; i++ {
time.Sleep(backOff)
err := signer.Broadcast(tx)
if err != nil {
logger.Warn().Err(err).Fields(lf).Msgf("Broadcasting Bitcoin tx, retry %d", i)
backOff *= 2
continue
}
logger.Info().Fields(lf).Msgf("Broadcast Bitcoin tx successfully")
zetaHash, err := zetacoreClient.PostOutboundTracker(
ctx,
chain.ChainId,
outboundTssNonce,
outboundHash,
)
if err != nil {
logger.Err(err).Fields(lf).Msgf("Unable to add Bitcoin outbound tracker")
}
lf[logs.FieldZetaTx] = zetaHash
logger.Info().Fields(lf).Msgf("Add Bitcoin outbound tracker successfully")
// Save successfully broadcasted transaction to btc chain observer
btcObserver.SaveBroadcastedTx(outboundHash, outboundTssNonce)
break // successful broadcast; no need to retry
}
}
}