diff --git a/funding/manager.go b/funding/manager.go index 417ad9cff19..b12d138b178 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -1544,6 +1544,18 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, return } + // Enforce BOLT-02: push_msat MUST be <= 1000 * funding_satoshis. + if msg.PushAmount > lnwire.NewMSatFromSatoshis(msg.FundingAmount) { + f.failFundingFlow( + peer, cid, + lnwallet.ErrPushAmountTooLarge( + msg.PushAmount, msg.FundingAmount, + ), + ) + + return + } + // Send the OpenChannel request to the ChannelAcceptor to determine // whether this node will accept the channel. chanReq := &chanacceptor.ChannelAcceptRequest{ diff --git a/funding/manager_test.go b/funding/manager_test.go index 4924eec7a39..13c52f50814 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -3887,6 +3887,78 @@ func TestFundingManagerRejectPush(t *testing.T) { ) } +// TestFundingManagerPushAmountExceedsCapacity asserts that the fundee +// rejects an incoming OpenChannel whose push_msat exceeds +// 1000 * funding_satoshis, as required by BOLT-02. +func TestFundingManagerPushAmountExceedsCapacity(t *testing.T) { + t.Parallel() + + alice, bob := setupFundingManagers(t) + t.Cleanup(func() { + tearDownFundingManagers(t, alice, bob) + }) + + // Kick off a normal funding workflow so Alice produces a + // well-formed OpenChannel message that we can then tamper with. + updateChan := make(chan *lnrpc.OpenStatusUpdate) + errChan := make(chan error, 1) + initReq := &InitFundingMsg{ + Peer: bob, + TargetPubkey: bob.privKey.PubKey(), + ChainHash: *fundingNetParams.GenesisHash, + LocalFundingAmt: 500000, + PushAmt: lnwire.NewMSatFromSatoshis(10), + Private: true, + Updates: updateChan, + Err: errChan, + } + + alice.fundingMgr.InitFundingWorkflow(initReq) + + // Intercept Alice's OpenChannel. + var aliceMsg lnwire.Message + select { + case aliceMsg = <-alice.msgChan: + case err := <-initReq.Err: + t.Fatalf("error init funding workflow: %v", err) + case <-time.After(time.Second * 5): + t.Fatalf("alice did not send OpenChannel message") + } + + openChannelReq, ok := aliceMsg.(*lnwire.OpenChannel) + if !ok { + errorMsg, gotError := aliceMsg.(*lnwire.Error) + if gotError { + t.Fatalf("expected OpenChannel to be sent "+ + "from alice, instead got error: %v", + errorMsg.Error()) + } + t.Fatalf("expected OpenChannel to be sent from "+ + "alice, instead got %T", aliceMsg) + } + + // Overwrite the push amount so it strictly exceeds + // 1000 * funding_satoshis. Alice's own funding flow would never + // produce this on the wire, so we inject it manually to exercise + // Bob's spec-level bound check. + openChannelReq.PushAmount = lnwire.NewMSatFromSatoshis( + openChannelReq.FundingAmount, + ) + 1 + + // Hand the tampered message to Bob. + bob.fundingMgr.ProcessFundingMsg(openChannelReq, alice) + + // Bob should respond with an Error that carries the + // ErrPushAmountTooLarge message. + msg := assertFundingMsgSent(t, bob.msgChan, "Error") + err, ok := msg.(*lnwire.Error) + require.True(t, ok, "expected *lnwire.Error, got %T", msg) + require.ErrorContains( + t, err, "exceeds funding amount", + "expected ErrPushAmountTooLarge error, got \"%v\"", err.Error(), + ) +} + // TestFundingManagerMaxConfs ensures that we don't accept a funding proposal // that proposes a MinAcceptDepth greater than the maximum number of // confirmations we're willing to accept. diff --git a/lnwallet/errors.go b/lnwallet/errors.go index b2d1a5c42eb..e21af48f324 100644 --- a/lnwallet/errors.go +++ b/lnwallet/errors.go @@ -85,6 +85,18 @@ func ErrNonZeroPushAmount() ReservationError { return ReservationError{errors.New("non-zero push amounts are disabled")} } +// ErrPushAmountTooLarge is returned when the push amount exceeds the channel's +// funding amount, which violates BOLT-02 (push_msat MUST be <= +// 1000 * funding_satoshis). +func ErrPushAmountTooLarge(pushAmt lnwire.MilliSatoshi, + fundingAmt btcutil.Amount) ReservationError { + + return ReservationError{ + fmt.Errorf("push amount %v exceeds funding amount %v", + pushAmt, lnwire.NewMSatFromSatoshis(fundingAmt)), + } +} + // ErrMinHtlcTooLarge returns an error indicating that the MinHTLC value the // remote required is too large to be accepted. func ErrMinHtlcTooLarge(minHtlc,