client: verify batch

This commit is contained in:
Oliver Gugger 2020-04-01 17:06:31 +02:00
parent 84d8379341
commit 42adb28ff5
9 changed files with 1571 additions and 66 deletions

View file

@ -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
View 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
View 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)

View 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
}

View file

@ -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
View 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
}

View file

@ -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

View file

@ -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,
},
},
})
}

View file

@ -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(