mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-13 12:33:04 +02:00
client: verify batch
This commit is contained in:
parent
84d8379341
commit
42adb28ff5
9 changed files with 1571 additions and 66 deletions
194
order/batch.go
194
order/batch.go
|
|
@ -1,5 +1,18 @@
|
|||
package order
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/agora/client/account"
|
||||
"github.com/lightninglabs/agora/client/clmrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
// BatchVersion is the type for the batch verification protocol.
|
||||
type BatchVersion uint32
|
||||
|
||||
|
|
@ -15,3 +28,184 @@ const (
|
|||
// will be detected during the OrderMatchPrepare call.
|
||||
CurrentVersion = DefaultVersion
|
||||
)
|
||||
|
||||
// BatchID is a 33-byte point that uniquely identifies this batch. This ID
|
||||
// will be used later for account key derivation when constructing the batch
|
||||
// execution transaction.
|
||||
type BatchID [33]byte
|
||||
|
||||
// AccountDiff represents a matching+clearing event for a trader's account.
|
||||
// This diff shows the total balance delta along with a breakdown for each item
|
||||
// for a trader's account.
|
||||
type AccountDiff struct {
|
||||
// AccountKeyRaw is the raw serialized account public key this diff
|
||||
// refers to.
|
||||
AccountKeyRaw [33]byte
|
||||
|
||||
// AccountKey is the parsed account public key this diff refers to.
|
||||
AccountKey *btcec.PublicKey
|
||||
|
||||
// EndingState is the ending on-chain state of the account after the
|
||||
// executed batch as the auctioneer calculated it.
|
||||
EndingState clmrpc.AccountDiff_AccountState
|
||||
|
||||
// EndingBalance is the ending balance for a trader's account.
|
||||
EndingBalance btcutil.Amount
|
||||
|
||||
// OutpointIndex is the index of the re-created account output in the
|
||||
// batch transaction. This is set to -1 if no account output has been
|
||||
// created because the leftover value was considered to be dust.
|
||||
OutpointIndex int32
|
||||
|
||||
// Expiry is the on-chain CSV expiry for the re-created account output.
|
||||
Expiry uint32
|
||||
}
|
||||
|
||||
// validateEndingState validates that the ending state of an account as
|
||||
// proposed by the server is correct.
|
||||
func (d *AccountDiff) validateEndingState(tx *wire.MsgTx,
|
||||
acct *account.Account) error {
|
||||
|
||||
state := d.EndingState
|
||||
wrongStateErr := fmt.Errorf(
|
||||
"unexpected state %d for ending balance %d", state,
|
||||
d.EndingBalance,
|
||||
)
|
||||
|
||||
// Depending on the final amount of the account, we might get
|
||||
// dust which is handled differently.
|
||||
if d.EndingBalance < MinNoDustAccountSize {
|
||||
// The ending balance of the account is too small to be spent
|
||||
// by a simple transaction and not create a dust output. We
|
||||
// expect the server to set the state correctly and not re-
|
||||
// create an account outpoint.
|
||||
if state != clmrpc.AccountDiff_OUTPUT_DUST_EXTENDED_OFFCHAIN &&
|
||||
state != clmrpc.AccountDiff_OUTPUT_DUST_ADDED_TO_FEES &&
|
||||
state != clmrpc.AccountDiff_OUTPUT_FULLY_SPENT {
|
||||
|
||||
return wrongStateErr
|
||||
}
|
||||
if d.OutpointIndex >= 0 {
|
||||
return fmt.Errorf("unexpected outpoint index for dust " +
|
||||
"account")
|
||||
}
|
||||
} else {
|
||||
// There should be enough balance left to justify a new account
|
||||
// output. We should get the outpoint from the server.
|
||||
if state != clmrpc.AccountDiff_OUTPUT_RECREATED {
|
||||
return wrongStateErr
|
||||
}
|
||||
if d.OutpointIndex < 0 {
|
||||
return fmt.Errorf("outpoint index invalid for non-"+
|
||||
"dust account with state %d and balance %d",
|
||||
state, d.EndingBalance)
|
||||
}
|
||||
|
||||
// Make sure the outpoint index is correct and there is an
|
||||
// output with the correct amount there.
|
||||
if d.OutpointIndex >= int32(len(tx.TxOut)) {
|
||||
return fmt.Errorf("outpoint index out of bounds")
|
||||
}
|
||||
out := tx.TxOut[d.OutpointIndex]
|
||||
if btcutil.Amount(out.Value) != d.EndingBalance {
|
||||
return fmt.Errorf("invalid account output amount. got "+
|
||||
"%d expected %d", out.Value, d.EndingBalance)
|
||||
}
|
||||
|
||||
// Final check, make sure we arrive at the same script for the
|
||||
// new account output.
|
||||
nextScript, err := acct.NextOutputScript()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive next account "+
|
||||
"script: %v", err)
|
||||
}
|
||||
if !bytes.Equal(out.PkScript, nextScript) {
|
||||
return fmt.Errorf("unexpected account output script")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Batch is all the information the auctioneer sends to each trader for them to
|
||||
// validate a batch execution.
|
||||
type Batch struct {
|
||||
// ID is the batch's unique ID. If multiple messages come in with the
|
||||
// same ID, they are to be considered to be the _same batch_ with
|
||||
// updated matches. Any previous version of a batch with that ID should
|
||||
// be discarded in that case.
|
||||
ID BatchID
|
||||
|
||||
// BatchVersion is the version of the batch verification protocol.
|
||||
Version BatchVersion
|
||||
|
||||
// MatchedOrders is a map between all trader's orders and the other
|
||||
// orders that were matched to them in the batch.
|
||||
MatchedOrders map[Nonce][]*MatchedOrder
|
||||
|
||||
// AccountDiffs is the calculated difference for each trader's account
|
||||
// that was involved in the batch.
|
||||
AccountDiffs []*AccountDiff
|
||||
|
||||
// ExecutionFee is the FeeSchedule that was used by the server to
|
||||
// calculate the execution fee.
|
||||
ExecutionFee FeeSchedule
|
||||
|
||||
// ClearingPrice is the fixed rate the orders were cleared at.
|
||||
ClearingPrice FixedRatePremium
|
||||
|
||||
// BatchTX is the complete batch transaction with all non-witness data
|
||||
// fully populated.
|
||||
BatchTX *wire.MsgTx
|
||||
|
||||
// BatchTxFeeRate is the miner fee rate in sat/kW that was chosen for
|
||||
// the batch transaction.
|
||||
BatchTxFeeRate chainfee.SatPerKWeight
|
||||
|
||||
// FeeRebate is the rebate that was offered to the trader if another
|
||||
// batch participant wanted to pay more fees for a faster confirmation.
|
||||
FeeRebate btcutil.Amount
|
||||
}
|
||||
|
||||
// MatchedOrder is the other side to one of our matched orders. It contains all
|
||||
// the information that is needed to validate the match and to start negotiating
|
||||
// the channel opening with the matched trader's node.
|
||||
type MatchedOrder struct {
|
||||
// Order contains the details of the other order as sent by the server.
|
||||
Order Order
|
||||
|
||||
// MultiSigKey is a key of the node creating the order that will be used
|
||||
// to craft the channel funding TX's 2-of-2 multi signature output.
|
||||
MultiSigKey [33]byte
|
||||
|
||||
// NodeKey is the identity public key of the node creating the order.
|
||||
NodeKey [33]byte
|
||||
|
||||
// NodeAddrs is the list of network addresses of the node creating the
|
||||
// order.
|
||||
NodeAddrs []net.Addr
|
||||
|
||||
// UnitsFilled is the number of units that were matched by this order.
|
||||
UnitsFilled SupplyUnit
|
||||
}
|
||||
|
||||
// BatchSignature is a map type that is keyed by a trader's account key and
|
||||
// contains the witness stack (slice of slice of bytes) for the input that
|
||||
// spends from the current account in a batch.
|
||||
type BatchSignature map[[33]byte][][]byte
|
||||
|
||||
// BatchVerifier is an interface that can verify a batch from the point of view
|
||||
// of the trader.
|
||||
type BatchVerifier interface {
|
||||
// Verify makes sure the batch prepared by the server is correct and
|
||||
// can be accepted by the trader.
|
||||
Verify(*Batch) error
|
||||
}
|
||||
|
||||
// BatchSigner is an interface that can sign for a trader's account inputs in
|
||||
// a batch.
|
||||
type BatchSigner interface {
|
||||
// Sign returns the witness stack of all account inputs in a batch that
|
||||
// belong to the trader.
|
||||
Sign(*Batch) (BatchSignature, error)
|
||||
}
|
||||
|
|
|
|||
93
order/batch_signer.go
Normal file
93
order/batch_signer.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/lightninglabs/agora/client/account"
|
||||
"github.com/lightninglabs/agora/client/clmscript"
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
)
|
||||
|
||||
// batchSigner is a type that implements the BatchSigner interface and can sign
|
||||
// for a trader's account inputs in a batch.
|
||||
type batchSigner struct {
|
||||
getAccount func(*btcec.PublicKey) (*account.Account, error)
|
||||
signer lndclient.SignerClient
|
||||
}
|
||||
|
||||
// Sign returns the witness stack of all account inputs in a batch that
|
||||
// belong to the trader.
|
||||
//
|
||||
// NOTE: This method is part of the BatchSigner interface.
|
||||
func (s *batchSigner) Sign(batch *Batch) (BatchSignature, error) {
|
||||
ourSigs := make(BatchSignature)
|
||||
hashes := txscript.NewTxSigHashes(batch.BatchTX)
|
||||
|
||||
// At this point we know that the accounts charged are correct. So we
|
||||
// can just go through them, find the corresponding input in the batch
|
||||
// TX and sign it.
|
||||
for _, acctDiff := range batch.AccountDiffs {
|
||||
// Get account from DB and make sure we can create the output.
|
||||
acct, err := s.getAccount(acctDiff.AccountKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account not found: %v", err)
|
||||
}
|
||||
acctOut, err := acct.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get account output: "+
|
||||
"%v", err)
|
||||
}
|
||||
var acctKey [33]byte
|
||||
copy(acctKey[:], acct.TraderKey.PubKey.SerializeCompressed())
|
||||
|
||||
// Find the input index we are going to sign.
|
||||
inputIndex := -1
|
||||
for idx, in := range batch.BatchTX.TxIn {
|
||||
if in.PreviousOutPoint == acct.OutPoint {
|
||||
inputIndex = idx
|
||||
}
|
||||
}
|
||||
if inputIndex == -1 {
|
||||
return nil, fmt.Errorf("account input not found")
|
||||
}
|
||||
|
||||
// Gather the remaining components required to sign the
|
||||
// transaction and sign it.
|
||||
traderKeyTweak := clmscript.TraderKeyTweak(
|
||||
acct.BatchKey, acct.Secret, acct.TraderKey.PubKey,
|
||||
)
|
||||
witnessScript, err := clmscript.AccountWitnessScript(
|
||||
acct.Expiry, acct.TraderKey.PubKey, acct.AuctioneerKey,
|
||||
acct.BatchKey, acct.Secret,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signDesc := &input.SignDescriptor{
|
||||
KeyDesc: *acct.TraderKey,
|
||||
SingleTweak: traderKeyTweak,
|
||||
WitnessScript: witnessScript,
|
||||
Output: acctOut,
|
||||
HashType: txscript.SigHashAll,
|
||||
InputIndex: inputIndex,
|
||||
SigHashes: hashes,
|
||||
}
|
||||
sigs, err := s.signer.SignOutputRaw(
|
||||
context.Background(), batch.BatchTX,
|
||||
[]*input.SignDescriptor{signDesc},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ourSigs[acctKey] = sigs
|
||||
}
|
||||
|
||||
return ourSigs, nil
|
||||
}
|
||||
|
||||
// A compile-time constraint to ensure batchSigner implements BatchSigner.
|
||||
var _ BatchSigner = (*batchSigner)(nil)
|
||||
334
order/batch_verifier.go
Normal file
334
order/batch_verifier.go
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
package order
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/agora/client/account"
|
||||
"github.com/lightninglabs/agora/client/clmrpc"
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
)
|
||||
|
||||
const (
|
||||
// deriveKeyTimeout is the number of seconds we allow the wallet to take
|
||||
// to derive a key.
|
||||
deriveKeyTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMismatchErr is the wrapped error that is returned if the batch
|
||||
// verification fails.
|
||||
ErrMismatchErr = errors.New("batch verification result mismatch")
|
||||
)
|
||||
|
||||
// MismatchErr is an error type that is returned if the batch verification on
|
||||
// the client does not come up with the same result as the server.
|
||||
type MismatchErr struct {
|
||||
msg string
|
||||
cause error
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error cause. This is always ErrMismatchErr so
|
||||
// we can compare any error returned by the batch verifier with errors.Is() but
|
||||
// still retain the context what exactly went wrong.
|
||||
func (m *MismatchErr) Unwrap() error {
|
||||
return ErrMismatchErr
|
||||
}
|
||||
|
||||
// Error returns the underlying error message.
|
||||
//
|
||||
// NOTE: This method is part of the error interface.
|
||||
func (m *MismatchErr) Error() string {
|
||||
if m.cause == nil {
|
||||
return m.msg
|
||||
}
|
||||
return fmt.Sprintf("%s: %v", m.msg, m.cause)
|
||||
}
|
||||
|
||||
// newMismatchErr return a new MismatchErr from the cause and the error message.
|
||||
func newMismatchErr(cause error, msg string, args ...interface{}) error {
|
||||
return &MismatchErr{
|
||||
msg: fmt.Sprintf(msg, args...),
|
||||
cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// batchVerifier is a type that implements BatchVerifier and can verify a batch
|
||||
// from the point of view of the trader.
|
||||
type batchVerifier struct {
|
||||
orderStore Store
|
||||
getAccount func(*btcec.PublicKey) (*account.Account, error)
|
||||
wallet lndclient.WalletKitClient
|
||||
ourNodePubkey [33]byte
|
||||
}
|
||||
|
||||
// Verify makes sure the batch prepared by the server is correct and can be
|
||||
// accepted by the trader.
|
||||
//
|
||||
// NOTE: This method is part of the BatchVerifier interface.
|
||||
func (v *batchVerifier) Verify(batch *Batch) error {
|
||||
// First of all, make sure we're using the same batch validation version
|
||||
// as the server. Otherwise we bail out of the batch. This should
|
||||
// already be handled when the client connects/authenticates. But
|
||||
// doesn't hurt to check again.
|
||||
if batch.Version != CurrentVersion {
|
||||
return ErrVersionMismatch
|
||||
}
|
||||
|
||||
// First go through all orders that were matched for us. We'll make sure
|
||||
// we know of the order and that the numbers check out on a high level.
|
||||
accounts := make(map[[33]byte]*AccountTally)
|
||||
for nonce, theirOrders := range batch.MatchedOrders {
|
||||
// Find our order in the database.
|
||||
ourOrder, err := v.orderStore.GetOrder(nonce)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order %x not found: %v", nonce, err)
|
||||
}
|
||||
|
||||
// We'll index our account tallies by the serialized form of
|
||||
// the account key so some copying is necessary first.
|
||||
var (
|
||||
acctKey = ourOrder.Details().AcctKey
|
||||
acctKeyRaw [33]byte
|
||||
)
|
||||
if acctKey == nil {
|
||||
return fmt.Errorf("account for order %x invalid", nonce)
|
||||
}
|
||||
copy(acctKeyRaw[:], acctKey.SerializeCompressed())
|
||||
|
||||
// Find the account the order spends from, if it isn't already
|
||||
// in the cache because another order spends from it.
|
||||
tally, ok := accounts[acctKeyRaw]
|
||||
if !ok {
|
||||
acct, err := v.getAccount(acctKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("account %x not found: %v",
|
||||
acctKeyRaw, err)
|
||||
}
|
||||
tally = &AccountTally{
|
||||
Account: acct,
|
||||
EndingBalance: acct.Value,
|
||||
}
|
||||
accounts[acctKeyRaw] = tally
|
||||
}
|
||||
|
||||
// Now that we know which of our orders were involved in the
|
||||
// match, we can start validating the match and tally up the
|
||||
// account balance, executed units and fee diffs.
|
||||
unitsFilled := SupplyUnit(0)
|
||||
for _, theirOrder := range theirOrders {
|
||||
// Verify order compatibility and fee structure.
|
||||
err = v.validateMatchedOrder(
|
||||
tally, ourOrder, theirOrder, batch.ExecutionFee,
|
||||
batch.ClearingPrice,
|
||||
)
|
||||
if err != nil {
|
||||
return newMismatchErr(
|
||||
err, "error matching against order %x",
|
||||
theirOrder.Order.Nonce(),
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure there is a channel output included in the
|
||||
// batch transaction that has the multisig script we
|
||||
// expect.
|
||||
err = v.validateChannelOutput(
|
||||
batch.BatchTX, ourOrder, theirOrder,
|
||||
)
|
||||
if err != nil {
|
||||
return newMismatchErr(
|
||||
err, "error finding channel output "+
|
||||
"for matched order %x",
|
||||
theirOrder.Order.Nonce(),
|
||||
)
|
||||
}
|
||||
|
||||
// The match looks good, one channel output more to pay
|
||||
// chain fees for.
|
||||
tally.NumChansCreated++
|
||||
unitsFilled += theirOrder.UnitsFilled
|
||||
}
|
||||
|
||||
// Last check is to make sure our order has not been over
|
||||
// filled somehow.
|
||||
if unitsFilled > ourOrder.Details().UnitsUnfulfilled {
|
||||
return &MismatchErr{
|
||||
msg: fmt.Sprintf("invalid units to be filled "+
|
||||
"for order %x. currently unfulfilled "+
|
||||
"%d, matched with %d in total",
|
||||
ourOrder.Nonce(),
|
||||
ourOrder.Details().UnitsUnfulfilled,
|
||||
unitsFilled,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we know all the accounts that were involved in the batch,
|
||||
// we can make sure we got a diff for each of them.
|
||||
for _, diff := range batch.AccountDiffs {
|
||||
// We only should get diffs for accounts that have orders in the
|
||||
// batch. If not, something's messed up.
|
||||
tally, ok := accounts[diff.AccountKeyRaw]
|
||||
if !ok {
|
||||
return &MismatchErr{
|
||||
msg: fmt.Sprintf("got diff for uninvolved "+
|
||||
"account %x", diff.AccountKeyRaw),
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we know how many channels were created from the
|
||||
// given account, let's also account for the chain fees.
|
||||
tally.ChainFees(batch.BatchTxFeeRate)
|
||||
|
||||
// Even if the account output is dust, we should arrive at the
|
||||
// same number with our tally as the server.
|
||||
if diff.EndingBalance != tally.EndingBalance {
|
||||
return &MismatchErr{
|
||||
msg: fmt.Sprintf("server sent unexpected "+
|
||||
"ending balance. got %d expected %d",
|
||||
diff.EndingBalance, tally.EndingBalance),
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the ending state of the account is correct.
|
||||
err := diff.validateEndingState(batch.BatchTX, tally.Account)
|
||||
if err != nil {
|
||||
return newMismatchErr(
|
||||
err, "account %x diff is incorrect",
|
||||
diff.AccountKeyRaw,
|
||||
)
|
||||
}
|
||||
|
||||
// The expiry should be the same as it was before if the account
|
||||
// output has been recreated (which means the expiry has been
|
||||
// reset as the CSV starts counting again in the new output).
|
||||
if diff.EndingState == clmrpc.AccountDiff_OUTPUT_RECREATED &&
|
||||
diff.Expiry != tally.Expiry {
|
||||
|
||||
return &MismatchErr{
|
||||
msg: fmt.Sprintf("account %x has invalid "+
|
||||
"expiry: %d", diff.AccountKeyRaw,
|
||||
diff.Expiry),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From what we can tell, the batch looks good. At least our part checks
|
||||
// out at this point.
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMatchedOrder validates our order against another trader's order and
|
||||
// tallies up our order's account balance.
|
||||
func (v *batchVerifier) validateMatchedOrder(tally *AccountTally,
|
||||
ourOrder Order, otherOrder *MatchedOrder, executionFee FeeSchedule,
|
||||
clearingPrice FixedRatePremium) error {
|
||||
|
||||
// Order type must be opposite.
|
||||
if otherOrder.Order.Type() == ourOrder.Type() {
|
||||
return fmt.Errorf("order %x matched same type "+
|
||||
"orders", ourOrder.Nonce())
|
||||
}
|
||||
|
||||
// Make sure we weren't matched to our own order.
|
||||
if otherOrder.NodeKey == v.ourNodePubkey {
|
||||
return fmt.Errorf("other order is an order from our node")
|
||||
}
|
||||
|
||||
// Verify that the durations overlap. Then tally up all the fees and
|
||||
// units that were paid/accrued in this matched order pair. We can
|
||||
// safely cast orders here because we made sure we have the right types
|
||||
// in the previous step.
|
||||
switch ours := ourOrder.(type) {
|
||||
case *Ask:
|
||||
other := otherOrder.Order.(*Bid)
|
||||
if other.MinDuration > ours.MaxDuration {
|
||||
return fmt.Errorf("order duration not overlapping " +
|
||||
"for our ask")
|
||||
}
|
||||
|
||||
// The ask's price cannot be higher than the bid's price.
|
||||
if ours.FixedRate > other.FixedRate {
|
||||
return fmt.Errorf("ask price greater than bid price")
|
||||
}
|
||||
|
||||
// This match checks out, deduct it from the account's balance.
|
||||
tally.CalcMakerDelta(
|
||||
executionFee, clearingPrice,
|
||||
otherOrder.UnitsFilled.ToSatoshis(), other.MinDuration,
|
||||
)
|
||||
|
||||
case *Bid:
|
||||
other := otherOrder.Order.(*Ask)
|
||||
if other.MaxDuration < ours.MinDuration {
|
||||
return fmt.Errorf("order duration not overlapping " +
|
||||
"for our bid")
|
||||
}
|
||||
|
||||
// The ask's price cannot be higher than the bid's price.
|
||||
if other.FixedRate > ours.FixedRate {
|
||||
return fmt.Errorf("ask price greater than bid price")
|
||||
}
|
||||
|
||||
// This match checks out, deduct it from the account's balance.
|
||||
tally.CalcTakerDelta(
|
||||
executionFee, clearingPrice,
|
||||
otherOrder.UnitsFilled.ToSatoshis(), ours.MinDuration,
|
||||
)
|
||||
}
|
||||
|
||||
// Everything checks out so far.
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateChannelOutput makes sure there is a channel output in the batch TX
|
||||
// that spends the correct amount for the matched units to the correct multisig
|
||||
// script that can be used by us to open the channel.
|
||||
func (v *batchVerifier) validateChannelOutput(batchTx *wire.MsgTx,
|
||||
ourOrder Order, otherOrder *MatchedOrder) error {
|
||||
|
||||
// Re-derive our multisig key first.
|
||||
ctxt, cancel := context.WithTimeout(
|
||||
context.Background(), deriveKeyTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
ourKey, err := v.wallet.DeriveKey(
|
||||
ctxt, &ourOrder.Details().MultiSigKeyLocator,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not derive our multisig key: %v", err)
|
||||
}
|
||||
|
||||
// Gather the information we expect to find in the batch TX.
|
||||
expectedOutputSize := otherOrder.UnitsFilled.ToSatoshis()
|
||||
_, expectedOut, err := input.GenFundingPkScript(
|
||||
ourKey.PubKey.SerializeCompressed(), otherOrder.MultiSigKey[:],
|
||||
int64(expectedOutputSize),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create multisig script: %v", err)
|
||||
}
|
||||
|
||||
// Locate the channel output now that we know what to look for.
|
||||
for _, out := range batchTx.TxOut {
|
||||
if out.Value == expectedOut.Value &&
|
||||
bytes.Equal(out.PkScript, expectedOut.PkScript) {
|
||||
|
||||
// Bingo, this is what we want.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("no channel output found in batch tx for matched "+
|
||||
"order %x", otherOrder.Order.Nonce())
|
||||
}
|
||||
|
||||
// A compile-time constraint to ensure batchVerifier implements BatchVerifier.
|
||||
var _ BatchVerifier = (*batchVerifier)(nil)
|
||||
449
order/batch_verifier_test.go
Normal file
449
order/batch_verifier_test.go
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/agora/client/account"
|
||||
"github.com/lightninglabs/agora/client/clmrpc"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
var (
|
||||
_, startBatchKey = btcec.PrivKeyFromBytes(btcec.S256(), []byte{0x01})
|
||||
_, acctKeyBig = btcec.PrivKeyFromBytes(btcec.S256(), []byte{0x02})
|
||||
_, acctKeySmall = btcec.PrivKeyFromBytes(btcec.S256(), []byte{0x03})
|
||||
nodePubkey = [33]byte{03, 77, 44, 55}
|
||||
execFeeBase = btcutil.Amount(1_100)
|
||||
execFeeRate = btcutil.Amount(50)
|
||||
clearingPrice = FixedRatePremium(5)
|
||||
stateRecreated = clmrpc.AccountDiff_OUTPUT_RECREATED
|
||||
stateExtendedOffchain = clmrpc.AccountDiff_OUTPUT_DUST_EXTENDED_OFFCHAIN
|
||||
)
|
||||
|
||||
func TestBatchVerifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
lnd = test.NewMockLnd()
|
||||
verifier = &batchVerifier{
|
||||
wallet: lnd.WalletKit,
|
||||
ourNodePubkey: nodePubkey,
|
||||
}
|
||||
batchID BatchID
|
||||
acctIDBig [33]byte
|
||||
acctIDSmall [33]byte
|
||||
)
|
||||
copy(batchID[:], startBatchKey.SerializeCompressed())
|
||||
copy(acctIDBig[:], acctKeyBig.SerializeCompressed())
|
||||
copy(acctIDSmall[:], acctKeySmall.SerializeCompressed())
|
||||
|
||||
// All the test cases we want to run. The doVerify function gets passed
|
||||
// in a batch that does pass validation and represents the happy path.
|
||||
// The test cases can manipulate the "good" batch specifically to
|
||||
// trigger validation edge cases.
|
||||
testCases := []struct {
|
||||
name string
|
||||
expectedErr string
|
||||
doVerify func(*Ask, *Bid, *Bid, *Batch) error
|
||||
}{
|
||||
{
|
||||
name: "version mismatch",
|
||||
expectedErr: ErrVersionMismatch.Error(),
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
return verifier.Verify(&Batch{Version: 999})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid order",
|
||||
expectedErr: "not found",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
arr := make([]*MatchedOrder, 0)
|
||||
b.MatchedOrders[Nonce{99, 99}] = arr
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid order type",
|
||||
expectedErr: "matched same type orders",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.MatchedOrders[a.nonce] = append(
|
||||
b.MatchedOrders[a.nonce],
|
||||
&MatchedOrder{
|
||||
Order: a,
|
||||
},
|
||||
)
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid node pubkey",
|
||||
expectedErr: "other order is an order from our node",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.MatchedOrders[a.nonce][0].NodeKey = nodePubkey
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ask max duration larger than bid",
|
||||
expectedErr: "duration not overlapping for our ask",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
a.MaxDuration = 100
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ask fixed rate larger than bid",
|
||||
expectedErr: "ask price greater than bid price",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
a.FixedRate = 20
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bid min duration larger than ask",
|
||||
expectedErr: "duration not overlapping for our bid",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
delete(b.MatchedOrders, a.nonce)
|
||||
b2.MinDuration = 5000
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bid fixed rate smaller than ask",
|
||||
expectedErr: "ask price greater than bid price",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
delete(b.MatchedOrders, a.nonce)
|
||||
b1.FixedRate = 5
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "channel output not found, wrong value",
|
||||
expectedErr: "no channel output found in batch tx for",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.BatchTX.TxOut[0].Value = 123
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "channel output not found, wrong script",
|
||||
expectedErr: "no channel output found in batch tx for",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.BatchTX.TxOut[0].PkScript = []byte{99, 88}
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid units filled",
|
||||
expectedErr: "invalid units to be filled for order",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.BatchTX.TxOut[0].Value = 900_000
|
||||
b.MatchedOrders[a.nonce][0].UnitsFilled = 9
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid funding TX fee rate",
|
||||
expectedErr: "server sent unexpected ending balance",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.BatchTxFeeRate *= 2
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid clearing price",
|
||||
expectedErr: "server sent unexpected ending balance",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.ClearingPrice *= 2
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid execution fee rate",
|
||||
expectedErr: "server sent unexpected ending balance",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.ExecutionFee = NewLinearFeeSchedule(1, 1)
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid ending state",
|
||||
expectedErr: "diff is incorrect: unexpected state",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.ExecutionFee = NewLinearFeeSchedule(0, 0)
|
||||
b.BatchTX.TxOut[2].Value += 2220
|
||||
b.AccountDiffs[0].EndingBalance += 2220
|
||||
b.AccountDiffs[1].EndingBalance += 2220
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid ending output",
|
||||
expectedErr: "diff is incorrect: outpoint index",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
b.ExecutionFee = NewLinearFeeSchedule(0, 0)
|
||||
b.BatchTX.TxOut[2].Value += 2220
|
||||
b.AccountDiffs[0].EndingBalance += 2220
|
||||
b.AccountDiffs[1].EndingBalance += 2220
|
||||
b.AccountDiffs[1].EndingState = stateRecreated
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path",
|
||||
expectedErr: "",
|
||||
doVerify: func(a *Ask, b1, b2 *Bid, b *Batch) error {
|
||||
return verifier.Verify(b)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Run through all the test cases, creating a new, valid batch each
|
||||
// time so no state carries over from the last run.
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
|
||||
// We'll create two accounts: A smaller one that has one ask for
|
||||
// 4 units that will be completely used up. Then a larger
|
||||
// account that has two bids that are both matched to the ask.
|
||||
// This account is large enough to be recreated, as it only
|
||||
// needs to pay for fees.
|
||||
bigAcct := &account.Account{
|
||||
TraderKey: &keychain.KeyDescriptor{
|
||||
PubKey: acctKeyBig,
|
||||
},
|
||||
Value: 500_000,
|
||||
Expiry: 144,
|
||||
State: account.StateOpen,
|
||||
BatchKey: startBatchKey,
|
||||
AuctioneerKey: startBatchKey,
|
||||
}
|
||||
smallAcct := &account.Account{
|
||||
TraderKey: &keychain.KeyDescriptor{
|
||||
PubKey: acctKeySmall,
|
||||
},
|
||||
Value: 400_000,
|
||||
Expiry: 144,
|
||||
State: account.StateOpen,
|
||||
BatchKey: startBatchKey,
|
||||
AuctioneerKey: startBatchKey,
|
||||
}
|
||||
ask := &Ask{
|
||||
Kit: newKitFromTemplate(Nonce{0x01}, &Kit{
|
||||
MultiSigKeyLocator: keychain.KeyLocator{
|
||||
Index: 0,
|
||||
},
|
||||
Units: 4,
|
||||
UnitsUnfulfilled: 4,
|
||||
AcctKey: acctKeySmall,
|
||||
FixedRate: 10,
|
||||
}),
|
||||
MaxDuration: 2500,
|
||||
}
|
||||
bid1 := &Bid{
|
||||
Kit: newKitFromTemplate(Nonce{0x02}, &Kit{
|
||||
MultiSigKeyLocator: keychain.KeyLocator{
|
||||
Index: 1,
|
||||
},
|
||||
Units: 2,
|
||||
UnitsUnfulfilled: 2,
|
||||
AcctKey: acctKeyBig,
|
||||
FixedRate: 15,
|
||||
}),
|
||||
// 2000 * (200_000 * 5 / 1_000_000) = 1000 sats premium
|
||||
MinDuration: 1000,
|
||||
}
|
||||
bid2 := &Bid{
|
||||
Kit: newKitFromTemplate(Nonce{0x03}, &Kit{
|
||||
MultiSigKeyLocator: keychain.KeyLocator{
|
||||
Index: 2,
|
||||
},
|
||||
Units: 8,
|
||||
UnitsUnfulfilled: 8,
|
||||
AcctKey: acctKeyBig,
|
||||
FixedRate: 15,
|
||||
}),
|
||||
// 2000 * (200_000 * 5 / 1_000_000) = 2000 sats premium
|
||||
MinDuration: 2000,
|
||||
}
|
||||
batchTx := &wire.MsgTx{
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
// Channel output for channel between ask and
|
||||
// bid1.
|
||||
{
|
||||
Value: 200_000,
|
||||
PkScript: scriptForChan(
|
||||
t, lnd, ask.MultiSigKeyLocator,
|
||||
bid1.MultiSigKeyLocator,
|
||||
),
|
||||
},
|
||||
// Channel output for channel between ask and
|
||||
// bid2.
|
||||
{
|
||||
Value: 200_000,
|
||||
PkScript: scriptForChan(
|
||||
t, lnd, ask.MultiSigKeyLocator,
|
||||
bid2.MultiSigKeyLocator,
|
||||
),
|
||||
},
|
||||
// Recreated account output for large account.
|
||||
{
|
||||
// balance - bid1Premium - bid2Premium -
|
||||
// bid1ExecFee - bid2ExecFee - chainFees
|
||||
// 500_000 - 1000 - 2000 -
|
||||
// 1_110 - 1_110 - 186
|
||||
Value: 494_594,
|
||||
PkScript: scriptForAcct(t, bigAcct),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create a batch for us as if we were the trader for both
|
||||
// accounts and were matched against each other (not impossible
|
||||
// but unlikely to happen in the real system).
|
||||
accountDiffs := []*AccountDiff{
|
||||
{
|
||||
AccountKeyRaw: acctIDBig,
|
||||
AccountKey: acctKeyBig,
|
||||
EndingState: stateRecreated,
|
||||
OutpointIndex: 2,
|
||||
Expiry: bigAcct.Expiry,
|
||||
EndingBalance: 494_594,
|
||||
},
|
||||
{
|
||||
AccountKeyRaw: acctIDSmall,
|
||||
AccountKey: acctKeySmall,
|
||||
EndingState: stateExtendedOffchain,
|
||||
OutpointIndex: -1,
|
||||
Expiry: smallAcct.Expiry,
|
||||
EndingBalance: 594,
|
||||
},
|
||||
}
|
||||
matchedOrders := map[Nonce][]*MatchedOrder{
|
||||
ask.nonce: {
|
||||
{
|
||||
Order: bid1,
|
||||
UnitsFilled: 2,
|
||||
MultiSigKey: deriveRawKey(
|
||||
t, lnd, bid1.MultiSigKeyLocator,
|
||||
),
|
||||
},
|
||||
{
|
||||
Order: bid2,
|
||||
UnitsFilled: 2,
|
||||
MultiSigKey: deriveRawKey(
|
||||
t, lnd, bid2.MultiSigKeyLocator,
|
||||
),
|
||||
},
|
||||
},
|
||||
bid1.nonce: {{
|
||||
Order: ask,
|
||||
UnitsFilled: 2,
|
||||
MultiSigKey: deriveRawKey(
|
||||
t, lnd, ask.MultiSigKeyLocator,
|
||||
),
|
||||
}},
|
||||
bid2.nonce: {{
|
||||
Order: ask,
|
||||
UnitsFilled: 2,
|
||||
MultiSigKey: deriveRawKey(
|
||||
t, lnd, ask.MultiSigKeyLocator,
|
||||
),
|
||||
}},
|
||||
}
|
||||
batch := &Batch{
|
||||
ID: batchID,
|
||||
Version: DefaultVersion,
|
||||
MatchedOrders: matchedOrders,
|
||||
AccountDiffs: accountDiffs,
|
||||
ExecutionFee: NewLinearFeeSchedule(
|
||||
execFeeBase, execFeeRate,
|
||||
),
|
||||
ClearingPrice: clearingPrice,
|
||||
BatchTX: batchTx,
|
||||
BatchTxFeeRate: chainfee.FeePerKwFloor,
|
||||
}
|
||||
|
||||
// Create the starting database state now.
|
||||
storeMock := newMockStore()
|
||||
verifier.orderStore = storeMock
|
||||
verifier.getAccount = storeMock.getAccount
|
||||
storeMock.accounts = map[*btcec.PublicKey]*account.Account{
|
||||
acctKeyBig: bigAcct,
|
||||
acctKeySmall: smallAcct,
|
||||
}
|
||||
storeMock.orders = map[Nonce]Order{
|
||||
ask.Nonce(): ask,
|
||||
bid1.Nonce(): bid1,
|
||||
bid2.Nonce(): bid2,
|
||||
}
|
||||
|
||||
// Finally run the test case itself.
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.doVerify(ask, bid1, bid2, batch)
|
||||
if (err == nil && tc.expectedErr != "") ||
|
||||
(err != nil && !strings.Contains(
|
||||
err.Error(), tc.expectedErr,
|
||||
)) {
|
||||
|
||||
t.Fatalf("unexpected error, got '%v' wanted "+
|
||||
"'%v'", err, tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newKitFromTemplate(nonce Nonce, tpl *Kit) Kit {
|
||||
kit := NewKit(nonce)
|
||||
kit.Version = tpl.Version
|
||||
kit.State = tpl.State
|
||||
kit.FixedRate = tpl.FixedRate
|
||||
kit.Amt = tpl.Amt
|
||||
kit.Units = tpl.Units
|
||||
kit.UnitsUnfulfilled = tpl.UnitsUnfulfilled
|
||||
kit.MultiSigKeyLocator = tpl.MultiSigKeyLocator
|
||||
kit.FundingFeeRate = tpl.FundingFeeRate
|
||||
kit.AcctKey = tpl.AcctKey
|
||||
return *kit
|
||||
}
|
||||
|
||||
func scriptForChan(t *testing.T, lnd *test.LndMockServices, loc1,
|
||||
loc2 keychain.KeyLocator) []byte {
|
||||
|
||||
key1 := deriveRawKey(t, lnd, loc1)
|
||||
key2 := deriveRawKey(t, lnd, loc2)
|
||||
_, out, err := input.GenFundingPkScript(key1[:], key2[:], 123)
|
||||
if err != nil {
|
||||
t.Fatalf("error generating funding script: %v", err)
|
||||
}
|
||||
return out.PkScript
|
||||
}
|
||||
|
||||
func deriveRawKey(t *testing.T, lnd *test.LndMockServices,
|
||||
loc keychain.KeyLocator) [33]byte {
|
||||
|
||||
key, err := lnd.WalletKit.DeriveKey(context.Background(), &loc)
|
||||
if err != nil {
|
||||
t.Fatalf("error deriving key: %v", err)
|
||||
}
|
||||
var rawKey [33]byte
|
||||
copy(rawKey[:], key.PubKey.SerializeCompressed())
|
||||
return rawKey
|
||||
}
|
||||
|
||||
func scriptForAcct(t *testing.T, acct *account.Account) []byte {
|
||||
script, err := acct.NextOutputScript()
|
||||
if err != nil {
|
||||
t.Errorf("error deriving next script: %v", err)
|
||||
}
|
||||
return script
|
||||
}
|
||||
|
|
@ -6,12 +6,19 @@ import (
|
|||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/agora/client/account"
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultLndTimeout is the default number of seconds we are willing to
|
||||
// wait for our lnd node to respond.
|
||||
defaultLndTimeout = time.Second * 30
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrVersionMismatch is the error that is returned if we don't
|
||||
// implement the same batch verification version as the server.
|
||||
|
|
@ -25,6 +32,8 @@ type ManagerConfig struct {
|
|||
// Store is responsible for storing and retrieving order information.
|
||||
Store Store
|
||||
|
||||
AcctStore account.Store
|
||||
|
||||
// Lightning is used to access the main RPC to get information about the
|
||||
// lnd node that agora is connected to.
|
||||
Lightning lndclient.LightningClient
|
||||
|
|
@ -45,6 +54,10 @@ type Manager struct {
|
|||
|
||||
wg sync.WaitGroup
|
||||
quit chan struct{}
|
||||
|
||||
batchVerifier BatchVerifier
|
||||
batchSigner BatchSigner
|
||||
pendingBatch *Batch
|
||||
}
|
||||
|
||||
// NewManager instantiates a new Manager backed by the given config.
|
||||
|
|
@ -58,7 +71,30 @@ func NewManager(cfg *ManagerConfig) *Manager {
|
|||
// Start starts all concurrent tasks the manager is responsible for.
|
||||
func (m *Manager) Start() error {
|
||||
var err error
|
||||
m.started.Do(func() {})
|
||||
m.started.Do(func() {
|
||||
// We'll need our node's identity public key for a bunch of
|
||||
// different validations so we might as well cache it on
|
||||
// startup as it cannot change.
|
||||
var info *lndclient.Info
|
||||
ctxt, cancel := context.WithTimeout(
|
||||
context.Background(), defaultLndTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
info, err = m.cfg.Lightning.GetInfo(ctxt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m.batchVerifier = &batchVerifier{
|
||||
orderStore: m.cfg.Store,
|
||||
getAccount: m.cfg.AcctStore.Account,
|
||||
wallet: m.cfg.Wallet,
|
||||
ourNodePubkey: info.IdentityPubkey,
|
||||
}
|
||||
m.batchSigner = &batchSigner{
|
||||
getAccount: m.cfg.AcctStore.Account,
|
||||
signer: m.cfg.Signer,
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +110,7 @@ func (m *Manager) Stop() {
|
|||
func (m *Manager) PrepareOrder(ctx context.Context, order Order,
|
||||
acct *account.Account) (*ServerOrderParams, error) {
|
||||
|
||||
// Validate incoming request for formal validity.
|
||||
// Verify incoming request for formal validity.
|
||||
err := m.validateOrder(order, acct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -175,6 +211,36 @@ func (m *Manager) validateOrder(order Order, acct *account.Account) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// OrderMatchValidate...
|
||||
func (m *Manager) OrderMatchValidate(batch *Batch) error {
|
||||
// Make sure we have no objection to the current batch. Then store
|
||||
// it in case it ends up being the final version.
|
||||
err := m.batchVerifier.Verify(batch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error validating batch: %v", err)
|
||||
}
|
||||
m.pendingBatch = batch
|
||||
|
||||
// TODO:
|
||||
// - cancel funding shim of previous pending batch if not nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchSign...
|
||||
func (m *Manager) BatchSign() (BatchSignature, error) {
|
||||
return m.batchSigner.Sign(m.pendingBatch)
|
||||
}
|
||||
|
||||
func (m *Manager) BatchFinalize(batchID BatchID) error {
|
||||
// TODO:
|
||||
// - update order DB
|
||||
// - update account DB
|
||||
// - call lnrpc.OpenChannel
|
||||
m.pendingBatch = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseNodeUris parses a list of node URIs in the format <pubkey>@addr:port
|
||||
// as it's returned in the `lnrpc.GetInfo` request.
|
||||
// TODO(guggero): What is needed to support tor as well?
|
||||
|
|
|
|||
292
order/rpc_parse.go
Normal file
292
order/rpc_parse.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
package order
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightninglabs/agora/client/clmrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
// ParseRPCOrder parses the incoming raw RPC order into the go native data
|
||||
// types used in the order struct.
|
||||
func ParseRPCOrder(version uint32, details *clmrpc.Order) (*Kit, error) {
|
||||
var nonce Nonce
|
||||
copy(nonce[:], details.OrderNonce)
|
||||
kit := NewKit(nonce)
|
||||
|
||||
// If the user didn't provide a nonce, we generate one.
|
||||
if nonce == ZeroNonce {
|
||||
preimageBytes, err := randomPreimage()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate nonce: %v", err)
|
||||
}
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], preimageBytes)
|
||||
kit = NewKitWithPreimage(preimage)
|
||||
}
|
||||
|
||||
pubKey, err := btcec.ParsePubKey(details.UserSubKey, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing account key: %v", err)
|
||||
}
|
||||
|
||||
kit.AcctKey = pubKey
|
||||
kit.Version = Version(version)
|
||||
kit.FixedRate = uint32(details.RateFixed)
|
||||
kit.Amt = btcutil.Amount(details.Amt)
|
||||
kit.FundingFeeRate = chainfee.SatPerKWeight(details.FundingFeeRate)
|
||||
kit.Units = NewSupplyFromSats(kit.Amt)
|
||||
kit.UnitsUnfulfilled = kit.Units
|
||||
return kit, nil
|
||||
}
|
||||
|
||||
// ParseRPCServerOrder parses the incoming raw RPC server order into the go
|
||||
// native data types used in the order struct.
|
||||
func ParseRPCServerOrder(version uint32, details *clmrpc.ServerOrder) (*Kit,
|
||||
[33]byte, []net.Addr, [33]byte, error) {
|
||||
|
||||
var (
|
||||
nonce Nonce
|
||||
nodeKey [33]byte
|
||||
nodeAddrs = make([]net.Addr, 0, len(details.NodeAddr))
|
||||
multiSigKey [33]byte
|
||||
)
|
||||
|
||||
copy(nonce[:], details.OrderNonce)
|
||||
kit := NewKit(nonce)
|
||||
kit.Version = Version(version)
|
||||
kit.FixedRate = uint32(details.RateFixed)
|
||||
kit.Amt = btcutil.Amount(details.Amt)
|
||||
kit.Units = NewSupplyFromSats(kit.Amt)
|
||||
kit.UnitsUnfulfilled = kit.Units
|
||||
kit.FundingFeeRate = chainfee.SatPerKWeight(
|
||||
details.FundingFeeRateSatPerKw,
|
||||
)
|
||||
|
||||
// If the user didn't provide a nonce, we generate one.
|
||||
if nonce == ZeroNonce {
|
||||
preimageBytes, err := randomPreimage()
|
||||
if err != nil {
|
||||
return nil, nodeKey, nodeAddrs, multiSigKey,
|
||||
fmt.Errorf("cannot generate nonce: %v", err)
|
||||
}
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], preimageBytes)
|
||||
kit = NewKitWithPreimage(preimage)
|
||||
}
|
||||
|
||||
pubKey, err := btcec.ParsePubKey(details.UserSubKey, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, nodeKey, nodeAddrs, multiSigKey,
|
||||
fmt.Errorf("error parsing account key: %v", err)
|
||||
}
|
||||
|
||||
kit.AcctKey = pubKey
|
||||
|
||||
nodePubKey, err := btcec.ParsePubKey(details.NodePub, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, nodeKey, nodeAddrs, multiSigKey,
|
||||
fmt.Errorf("unable to parse node pub key: %v",
|
||||
err)
|
||||
}
|
||||
copy(nodeKey[:], nodePubKey.SerializeCompressed())
|
||||
if len(details.NodeAddr) == 0 {
|
||||
return nil, nodeKey, nodeAddrs, multiSigKey,
|
||||
fmt.Errorf("invalid node addresses")
|
||||
}
|
||||
for _, rpcAddr := range details.NodeAddr {
|
||||
addr, err := net.ResolveTCPAddr(rpcAddr.Network, rpcAddr.Addr)
|
||||
if err != nil {
|
||||
return nil, nodeKey, nodeAddrs, multiSigKey,
|
||||
fmt.Errorf("unable to parse node ddr: %v", err)
|
||||
}
|
||||
nodeAddrs = append(nodeAddrs, addr)
|
||||
}
|
||||
multiSigPubkey, err := btcec.ParsePubKey(
|
||||
details.MultiSigKey, btcec.S256(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nodeKey, nodeAddrs, multiSigKey,
|
||||
fmt.Errorf("unable to parse multi sig pub key: %v", err)
|
||||
}
|
||||
copy(multiSigKey[:], multiSigPubkey.SerializeCompressed())
|
||||
|
||||
return kit, nodeKey, nodeAddrs, multiSigKey, nil
|
||||
}
|
||||
|
||||
// ParseRPCServerAsk parses the incoming raw RPC server ask into the go
|
||||
// native data types used in the order struct.
|
||||
func ParseRPCServerAsk(details *clmrpc.ServerAsk) (*MatchedOrder, error) {
|
||||
var (
|
||||
o = &MatchedOrder{}
|
||||
kit *Kit
|
||||
err error
|
||||
)
|
||||
kit, o.NodeKey, o.NodeAddrs, o.MultiSigKey, err =
|
||||
ParseRPCServerOrder(details.Version, details.Details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Order = &Ask{
|
||||
Kit: *kit,
|
||||
MaxDuration: uint32(details.MaxDurationBlocks),
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// ParseRPCServerBid parses the incoming raw RPC server bid into the go
|
||||
// native data types used in the order struct.
|
||||
func ParseRPCServerBid(details *clmrpc.ServerBid) (*MatchedOrder, error) {
|
||||
var (
|
||||
o = &MatchedOrder{}
|
||||
kit *Kit
|
||||
err error
|
||||
)
|
||||
kit, o.NodeKey, o.NodeAddrs, o.MultiSigKey, err =
|
||||
ParseRPCServerOrder(details.Version, details.Details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.Order = &Bid{
|
||||
Kit: *kit,
|
||||
MinDuration: uint32(details.MinDurationBlocks),
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// ParseRPCBatch parses the incoming raw RPC batch into the go native data types
|
||||
// used by the order manager.
|
||||
func ParseRPCBatch(prepareMsg *clmrpc.OrderMatchPrepare) (*Batch,
|
||||
error) {
|
||||
|
||||
b := &Batch{
|
||||
Version: BatchVersion(prepareMsg.BatchVersion),
|
||||
MatchedOrders: make(map[Nonce][]*MatchedOrder),
|
||||
BatchTX: &wire.MsgTx{},
|
||||
}
|
||||
|
||||
// Parse matched orders.
|
||||
for ourOrderHex, rpcMatchedOrders := range prepareMsg.MatchedOrders {
|
||||
var ourOrder Nonce
|
||||
ourOrderBytes, err := hex.DecodeString(ourOrderHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing nonce: %v", err)
|
||||
}
|
||||
copy(ourOrder[:], ourOrderBytes)
|
||||
b.MatchedOrders[ourOrder], err = ParseRPCMatchedOrders(
|
||||
rpcMatchedOrders,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing matched order: "+
|
||||
"%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse account diff.
|
||||
for _, diff := range prepareMsg.ChargedAccounts {
|
||||
var acctKeyRaw [33]byte
|
||||
acctKey, err := btcec.ParsePubKey(diff.UserSubKey, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing account key: %v",
|
||||
err)
|
||||
}
|
||||
copy(acctKeyRaw[:], acctKey.SerializeCompressed())
|
||||
b.AccountDiffs = append(
|
||||
b.AccountDiffs, &AccountDiff{
|
||||
AccountKeyRaw: acctKeyRaw,
|
||||
AccountKey: acctKey,
|
||||
EndingState: diff.EndingState,
|
||||
EndingBalance: btcutil.Amount(diff.EndingBalance),
|
||||
OutpointIndex: diff.OutpointIndex,
|
||||
Expiry: diff.Expiry,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Parse batch transaction.
|
||||
err := b.BatchTX.Deserialize(bytes.NewReader(
|
||||
prepareMsg.BatchTransaction,
|
||||
))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing batch TX: %v", err)
|
||||
}
|
||||
|
||||
// Convert clearing price, fee rate and rebate.
|
||||
b.ClearingPrice = FixedRatePremium(prepareMsg.ClearingPriceRate)
|
||||
b.BatchTxFeeRate = chainfee.SatPerKWeight(prepareMsg.FeeRateSatPerKw)
|
||||
b.FeeRebate = btcutil.Amount(prepareMsg.FeeRebateSat)
|
||||
|
||||
// Parse the execution fee.
|
||||
if prepareMsg.ExecutionFee == nil {
|
||||
return nil, fmt.Errorf("execution fee missing")
|
||||
}
|
||||
b.ExecutionFee = NewLinearFeeSchedule(
|
||||
btcutil.Amount(prepareMsg.ExecutionFee.BaseFee),
|
||||
btcutil.Amount(prepareMsg.ExecutionFee.FeeRate),
|
||||
)
|
||||
|
||||
// Parse the batch ID as public key just to make sure it's valid.
|
||||
_, err = btcec.ParsePubKey(prepareMsg.BatchId, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing batch ID: %v", err)
|
||||
}
|
||||
copy(b.ID[:], prepareMsg.BatchId)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// ParseRPCMatchedOrders parses the incoming raw RPC matched orders into the go
|
||||
// native structs used by the order manager.
|
||||
func ParseRPCMatchedOrders(orders *clmrpc.MatchedOrder) ([]*MatchedOrder,
|
||||
error) {
|
||||
|
||||
var result []*MatchedOrder
|
||||
// The only thing we can check in this step is that not both matched
|
||||
// bids and matched asks are set at the same time as that wouldn't make
|
||||
// sense. Everything else is checked at a later stage when we know more
|
||||
// about our order that was matched against.
|
||||
switch {
|
||||
case len(orders.MatchedAsks) > 0 && len(orders.MatchedBids) > 0:
|
||||
return nil, fmt.Errorf("order cannot match both asks and bids")
|
||||
|
||||
case len(orders.MatchedAsks) > 0:
|
||||
for _, ask := range orders.MatchedAsks {
|
||||
matchedAsk, err := ParseRPCServerAsk(ask.Ask)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing server "+
|
||||
"ask: %v", err)
|
||||
}
|
||||
result = append(result, matchedAsk)
|
||||
}
|
||||
|
||||
case len(orders.MatchedBids) > 0:
|
||||
for _, bid := range orders.MatchedBids {
|
||||
matchedBid, err := ParseRPCServerBid(bid.Bid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing server "+
|
||||
"bid: %v", err)
|
||||
}
|
||||
result = append(result, matchedBid)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// randomPreimage creates a new preimage from a random number generator.
|
||||
func randomPreimage() ([]byte, error) {
|
||||
var nonce Nonce
|
||||
_, err := rand.Read(nonce[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nonce[:], nil
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/btcsuite/btcd/blockchain"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcwallet/wallet/txrules"
|
||||
"github.com/lightninglabs/agora/client/account"
|
||||
"github.com/lightninglabs/agora/client/clmscript"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
|
|
@ -150,6 +151,9 @@ func EstimateTraderFee(numTraderChans uint32,
|
|||
// AccountTally keeps track of an account's balance and fees for all orders in
|
||||
// a batch that spend from/use that account.
|
||||
type AccountTally struct {
|
||||
// Account is the embedded account this tally is related to.
|
||||
*account.Account
|
||||
|
||||
// EndingBalance is the ending balance for a trader's account.
|
||||
EndingBalance btcutil.Amount
|
||||
|
||||
|
|
|
|||
192
rpcserver.go
192
rpcserver.go
|
|
@ -2,8 +2,8 @@ package client
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -21,8 +21,6 @@ import (
|
|||
"github.com/lightninglabs/agora/client/clmrpc"
|
||||
"github.com/lightninglabs/agora/client/order"
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -79,6 +77,7 @@ func newRPCServer(server *Server, serverDir string) (*rpcServer, error) {
|
|||
}),
|
||||
orderManager: order.NewManager(&order.ManagerConfig{
|
||||
Store: db,
|
||||
AcctStore: db,
|
||||
Lightning: lnd.Client,
|
||||
Wallet: lnd.WalletKit,
|
||||
Signer: lnd.Signer,
|
||||
|
|
@ -248,39 +247,63 @@ func (s *rpcServer) handleServerMessage(rpcMsg *clmrpc.ServerAuctionMessage) err
|
|||
switch msg := rpcMsg.Msg.(type) {
|
||||
// A new batch has been assembled with some of our orders.
|
||||
case *clmrpc.ServerAuctionMessage_Prepare:
|
||||
// Parse and formally validate what we got from the server.
|
||||
log.Tracef("Received prepare msg from server, batch_id=%x: %v",
|
||||
msg.Prepare.BatchId, spew.Sdump(msg))
|
||||
|
||||
// TODO(guggero): Add real batch validation here.
|
||||
// For now, we just send the accept back.
|
||||
err := s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
|
||||
Msg: &clmrpc.ClientAuctionMessage_Accept{
|
||||
Accept: &clmrpc.OrderMatchAccept{
|
||||
BatchId: msg.Prepare.BatchId,
|
||||
},
|
||||
},
|
||||
})
|
||||
batch, err := order.ParseRPCBatch(msg.Prepare)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error parsing RPC batch: %v", err)
|
||||
}
|
||||
|
||||
// TODO(guggero): Initiate channel opening negotiation with
|
||||
// remote peer here.
|
||||
err = s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
|
||||
Msg: &clmrpc.ClientAuctionMessage_Sign{
|
||||
Sign: &clmrpc.OrderMatchSign{
|
||||
BatchId: msg.Prepare.BatchId,
|
||||
},
|
||||
},
|
||||
})
|
||||
// Do an in-depth verification of the batch.
|
||||
err = s.orderManager.OrderMatchValidate(batch)
|
||||
if err != nil {
|
||||
return err
|
||||
// We can't accept the batch, something went wrong.
|
||||
log.Errorf("Error validating batch: %v", err)
|
||||
return s.sendRejectBatch(batch, err)
|
||||
}
|
||||
|
||||
// Accept the match now.
|
||||
//
|
||||
// TODO(guggero): Give user the option to bail out of any order
|
||||
// up to this point?
|
||||
err = s.sendAcceptBatch(batch)
|
||||
if err != nil {
|
||||
log.Errorf("Error sending accept msg: %v", err)
|
||||
return s.sendRejectBatch(batch, err)
|
||||
}
|
||||
|
||||
// We were able to accept the batch. Inform the auctioneer, then
|
||||
// start negotiating with the remote peers. We'll sign once all
|
||||
// channel partners have responded.
|
||||
err = s.server.BatchChannelSetup(batch)
|
||||
if err != nil {
|
||||
log.Errorf("Error setting up channels: %v", err)
|
||||
return s.sendRejectBatch(batch, err)
|
||||
}
|
||||
|
||||
// Sign for the accounts in the batch.
|
||||
sigs, err := s.orderManager.BatchSign()
|
||||
if err != nil {
|
||||
log.Errorf("Error signing batch: %v", err)
|
||||
return s.sendRejectBatch(batch, err)
|
||||
}
|
||||
err = s.sendSignBatch(batch, sigs)
|
||||
if err != nil {
|
||||
log.Errorf("Error sending sign msg: %v", err)
|
||||
return s.sendRejectBatch(batch, err)
|
||||
}
|
||||
|
||||
// The previously prepared batch has been executed and we can finalize
|
||||
// it by opening the channel and persisting the account and order diffs.
|
||||
case *clmrpc.ServerAuctionMessage_Finalize:
|
||||
log.Tracef("Received finalize msg from server, batch_id=%x: %v",
|
||||
msg.Finalize.BatchId, spew.Sdump(msg))
|
||||
|
||||
var batchID order.BatchID
|
||||
copy(batchID[:], msg.Finalize.BatchId)
|
||||
return s.orderManager.BatchFinalize(batchID)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown server message: %v", msg)
|
||||
}
|
||||
|
|
@ -419,7 +442,7 @@ func (s *rpcServer) SubmitOrder(ctx context.Context,
|
|||
switch requestOrder := req.Details.(type) {
|
||||
case *clmrpc.SubmitOrderRequest_Ask:
|
||||
a := requestOrder.Ask
|
||||
kit, err := parseRPCOrder(a.Version, a.Details)
|
||||
kit, err := order.ParseRPCOrder(a.Version, a.Details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -430,7 +453,7 @@ func (s *rpcServer) SubmitOrder(ctx context.Context,
|
|||
|
||||
case *clmrpc.SubmitOrderRequest_Bid:
|
||||
b := requestOrder.Bid
|
||||
kit, err := parseRPCOrder(b.Version, b.Details)
|
||||
kit, err := order.ParseRPCOrder(b.Version, b.Details)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -572,45 +595,86 @@ func (s *rpcServer) CancelOrder(ctx context.Context,
|
|||
return &clmrpc.CancelOrderResponse{}, nil
|
||||
}
|
||||
|
||||
// parseRPCOrder parses the incoming raw RPC order into the go native data
|
||||
// types used in the order struct.
|
||||
func parseRPCOrder(version uint32, details *clmrpc.Order) (*order.Kit, error) {
|
||||
var nonce order.Nonce
|
||||
copy(nonce[:], details.OrderNonce)
|
||||
kit := order.NewKit(nonce)
|
||||
// sendRejectBatch sends a reject message to the server with the properly
|
||||
// decoded reason code and the full reason message as a string.
|
||||
func (s *rpcServer) sendRejectBatch(batch *order.Batch, failure error) error {
|
||||
msg := &clmrpc.ClientAuctionMessage_Reject{
|
||||
Reject: &clmrpc.OrderMatchReject{
|
||||
BatchId: batch.ID[:],
|
||||
Reason: failure.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
// If the user didn't provide a nonce, we generate one.
|
||||
if nonce == order.ZeroNonce {
|
||||
preimageBytes, err := randomPreimage()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot generate nonce: %v", err)
|
||||
// Attach the status code to the message to give a bit more context.
|
||||
switch {
|
||||
case errors.Is(failure, order.ErrVersionMismatch):
|
||||
msg.Reject.ReasonCode = clmrpc.OrderMatchReject_BATCH_VERSION_MISMATCH
|
||||
|
||||
case errors.Is(failure, order.ErrMismatchErr):
|
||||
msg.Reject.ReasonCode = clmrpc.OrderMatchReject_SERVER_MISBEHAVIOR
|
||||
|
||||
default:
|
||||
msg.Reject.ReasonCode = clmrpc.OrderMatchReject_UNKNOWN
|
||||
}
|
||||
log.Infof("Sending batch rejection message for batch %x with "+
|
||||
"code %v and message: %v", batch.ID, msg.Reject.ReasonCode,
|
||||
failure)
|
||||
|
||||
// Send the message to the server. If a new error happens we return that
|
||||
// one because we know the causing error has at least been logged at
|
||||
// some point before.
|
||||
err := s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
|
||||
Msg: msg,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending reject message: %v", err)
|
||||
}
|
||||
return failure
|
||||
}
|
||||
|
||||
// sendAcceptBatch sends an accept message to the server with the list of order
|
||||
// nonces that we accept in the batch.
|
||||
func (s *rpcServer) sendAcceptBatch(batch *order.Batch) error {
|
||||
// Prepare the list of nonces we accept by serializing them to a slice
|
||||
// of byte slices.
|
||||
nonces := make([][]byte, 0, len(batch.MatchedOrders))
|
||||
idx := 0
|
||||
for nonce := range batch.MatchedOrders {
|
||||
nonces[idx] = nonce[:]
|
||||
idx++
|
||||
}
|
||||
|
||||
// Send the message to the server.
|
||||
return s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
|
||||
Msg: &clmrpc.ClientAuctionMessage_Accept{
|
||||
Accept: &clmrpc.OrderMatchAccept{
|
||||
BatchId: batch.ID[:],
|
||||
OrderNonce: nonces,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// sendSignBatch sends a sign message to the server with the witness stacks of
|
||||
// all accounts that are involved in the batch.
|
||||
func (s *rpcServer) sendSignBatch(batch *order.Batch,
|
||||
sigs order.BatchSignature) error {
|
||||
|
||||
// Prepare the list of witness stack messages and send them to the
|
||||
// server.
|
||||
rpcSigs := make(map[string]*clmrpc.AccountWitness)
|
||||
for acctKey, witness := range sigs {
|
||||
key := hex.EncodeToString(acctKey[:])
|
||||
rpcSigs[key] = &clmrpc.AccountWitness{
|
||||
Witness: witness,
|
||||
}
|
||||
var preimage lntypes.Preimage
|
||||
copy(preimage[:], preimageBytes)
|
||||
kit = order.NewKitWithPreimage(preimage)
|
||||
}
|
||||
|
||||
pubKey, err := btcec.ParsePubKey(details.UserSubKey, btcec.S256())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing account key: %v", err)
|
||||
}
|
||||
|
||||
kit.AcctKey = pubKey
|
||||
kit.Version = order.Version(version)
|
||||
kit.FixedRate = uint32(details.RateFixed)
|
||||
kit.Amt = btcutil.Amount(details.Amt)
|
||||
kit.FundingFeeRate = chainfee.SatPerKWeight(details.FundingFeeRate)
|
||||
kit.Units = order.NewSupplyFromSats(kit.Amt)
|
||||
kit.UnitsUnfulfilled = kit.Units
|
||||
return kit, nil
|
||||
}
|
||||
|
||||
// randomPreimage creates a new preimage from a random number generator.
|
||||
func randomPreimage() ([]byte, error) {
|
||||
var nonce order.Nonce
|
||||
_, err := rand.Read(nonce[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nonce[:], nil
|
||||
return s.auctioneer.SendAuctionMessage(&clmrpc.ClientAuctionMessage{
|
||||
Msg: &clmrpc.ClientAuctionMessage_Sign{
|
||||
Sign: &clmrpc.OrderMatchSign{
|
||||
BatchId: batch.ID[:],
|
||||
AccountWitness: rpcSigs,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
proxy "github.com/grpc-ecosystem/grpc-gateway/runtime"
|
||||
"github.com/lightninglabs/agora/client/auctioneer"
|
||||
"github.com/lightninglabs/agora/client/clmrpc"
|
||||
"github.com/lightninglabs/agora/client/order"
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightninglabs/loop/lsat"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
|
|
@ -230,6 +231,14 @@ func (s *Server) Stop() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// BatchChannelSetup...
|
||||
func (s *Server) BatchChannelSetup(batch *order.Batch) error {
|
||||
// TODO:
|
||||
// - connect to peers of new channels
|
||||
// - register funding shim in lnd
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLnd returns an instance of the lnd services proxy.
|
||||
func getLnd(network string, cfg *LndConfig) (*lndclient.GrpcLndServices, error) {
|
||||
return lndclient.NewLndServices(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue