diff --git a/channeldb/graph.go b/channeldb/graph.go index 1e6dbf8d62..b5326bb8cc 100644 --- a/channeldb/graph.go +++ b/channeldb/graph.go @@ -22,6 +22,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/aliasmgr" "github.com/lightningnetwork/lnd/batch" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -3731,35 +3732,17 @@ func (c *ChannelGraph) IsPublicNode(pubKey [33]byte) (bool, error) { // genMultiSigP2WSH generates the p2wsh'd multisig script for 2 of 2 pubkeys. func genMultiSigP2WSH(aPub, bPub []byte) ([]byte, error) { - if len(aPub) != 33 || len(bPub) != 33 { - return nil, fmt.Errorf("pubkey size error. Compressed " + - "pubkeys only") - } - - // Swap to sort pubkeys if needed. Keys are sorted in lexicographical - // order. The signatures within the scriptSig must also adhere to the - // order, ensuring that the signatures for each public key appears in - // the proper order on the stack. - if bytes.Compare(aPub, bPub) == 1 { - aPub, bPub = bPub, aPub - } - - // First, we'll generate the witness script for the multi-sig. - bldr := txscript.NewScriptBuilder() - bldr.AddOp(txscript.OP_2) - bldr.AddData(aPub) // Add both pubkeys (sorted). - bldr.AddData(bPub) - bldr.AddOp(txscript.OP_2) - bldr.AddOp(txscript.OP_CHECKMULTISIG) - witnessScript, err := bldr.Script() + witnessScript, err := input.GenMultiSigScript(aPub, bPub) if err != nil { return nil, err } - // With the witness script generated, we'll now turn it into a p2sh + // With the witness script generated, we'll now turn it into a p2wsh // script: // * OP_0 - bldr = txscript.NewScriptBuilder() + bldr := txscript.NewScriptBuilder( + txscript.WithScriptAllocSize(input.P2WSHSize), + ) bldr.AddOp(txscript.OP_0) scriptHash := sha256.Sum256(witnessScript) bldr.AddData(scriptHash[:]) diff --git a/docs/release-notes/release-notes-0.16.1.md b/docs/release-notes/release-notes-0.16.1.md index f3d1c48a57..7fc43e7192 100644 --- a/docs/release-notes/release-notes-0.16.1.md +++ b/docs/release-notes/release-notes-0.16.1.md @@ -40,7 +40,10 @@ https://github.com/lightningnetwork/lnd/pull/7359) * [Return `FEE_INSUFFICIENT` before checking balance for incoming low-fee HTLCs.](https://github.com/lightningnetwork/lnd/pull/7490). - + +* Optimize script allocation size in order to save + [memory](https://github.com/lightningnetwork/lnd/pull/7464). + ## Spec * [Add test vectors for diff --git a/go.mod b/go.mod index ac929db6da..7d052a9101 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/lightningnetwork/lnd require ( github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 - github.com/btcsuite/btcd v0.23.5-0.20230125025938-be056b0a0b2f + github.com/btcsuite/btcd v0.23.5-0.20230228185050-38331963bddd github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcutil v1.1.3 github.com/btcsuite/btcd/btcutil/psbt v1.1.8 diff --git a/go.sum b/go.sum index 5a6c84677d..eddeb11330 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7a github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd v0.23.3/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= -github.com/btcsuite/btcd v0.23.5-0.20230125025938-be056b0a0b2f h1:UJ/S/pV25+YsK0CJRJh8RDpTgy5h1oXWjOd4fp+opvY= -github.com/btcsuite/btcd v0.23.5-0.20230125025938-be056b0a0b2f/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.5-0.20230228185050-38331963bddd h1:v2Bs8yLhwv+8XYU96OOnhZ5BEj8ZlNGownexUO0e80I= +github.com/btcsuite/btcd v0.23.5-0.20230228185050-38331963bddd/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= diff --git a/input/script_utils.go b/input/script_utils.go index d625e69dc0..7e9345ef9d 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -35,7 +35,9 @@ type Signature interface { // WitnessScriptHash generates a pay-to-witness-script-hash public key script // paying to a version 0 witness program paying to the passed redeem script. func WitnessScriptHash(witnessScript []byte) ([]byte, error) { - bldr := txscript.NewScriptBuilder() + bldr := txscript.NewScriptBuilder( + txscript.WithScriptAllocSize(P2WSHSize), + ) bldr.AddOp(txscript.OP_0) scriptHash := sha256.Sum256(witnessScript) @@ -47,7 +49,9 @@ func WitnessScriptHash(witnessScript []byte) ([]byte, error) { // paying to a version 0 witness program containing the passed serialized // public key. func WitnessPubKeyHash(pubkey []byte) ([]byte, error) { - bldr := txscript.NewScriptBuilder() + bldr := txscript.NewScriptBuilder( + txscript.WithScriptAllocSize(P2WPKHSize), + ) bldr.AddOp(txscript.OP_0) pkhash := btcutil.Hash160(pubkey) @@ -58,7 +62,9 @@ func WitnessPubKeyHash(pubkey []byte) ([]byte, error) { // GenerateP2SH generates a pay-to-script-hash public key script paying to the // passed redeem script. func GenerateP2SH(script []byte) ([]byte, error) { - bldr := txscript.NewScriptBuilder() + bldr := txscript.NewScriptBuilder( + txscript.WithScriptAllocSize(NestedP2WPKHSize), + ) bldr.AddOp(txscript.OP_HASH160) scripthash := btcutil.Hash160(script) @@ -70,7 +76,9 @@ func GenerateP2SH(script []byte) ([]byte, error) { // GenerateP2PKH generates a pay-to-public-key-hash public key script paying to // the passed serialized public key. func GenerateP2PKH(pubkey []byte) ([]byte, error) { - bldr := txscript.NewScriptBuilder() + bldr := txscript.NewScriptBuilder( + txscript.WithScriptAllocSize(P2PKHSize), + ) bldr.AddOp(txscript.OP_DUP) bldr.AddOp(txscript.OP_HASH160) @@ -96,7 +104,8 @@ func GenerateUnknownWitness() ([]byte, error) { // pubkeys. func GenMultiSigScript(aPub, bPub []byte) ([]byte, error) { if len(aPub) != 33 || len(bPub) != 33 { - return nil, fmt.Errorf("pubkey size error: compressed pubkeys only") + return nil, fmt.Errorf("pubkey size error: compressed " + + "pubkeys only") } // Swap to sort pubkeys if needed. Keys are sorted in lexicographical @@ -107,7 +116,9 @@ func GenMultiSigScript(aPub, bPub []byte) ([]byte, error) { aPub, bPub = bPub, aPub } - bldr := txscript.NewScriptBuilder() + bldr := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + MultiSigSize, + )) bldr.AddOp(txscript.OP_2) bldr.AddData(aPub) // Add both pubkeys (sorted). bldr.AddData(bPub) @@ -241,7 +252,9 @@ func SenderHTLCScript(senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, paymentHash []byte, confirmedSpend bool) ([]byte, error) { - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + OfferedHtlcScriptSizeConfirmed, + )) // The opening operations are used to determine if this is the receiver // of the HTLC attempting to sweep all the funds due to a contract @@ -484,7 +497,9 @@ func ReceiverHTLCScript(cltvExpiry uint32, senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, paymentHash []byte, confirmedSpend bool) ([]byte, error) { - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + AcceptedHtlcScriptSizeConfirmed, + )) // The opening operations are used to determine if this is the sender // of the HTLC attempting to sweep all the funds due to a contract @@ -747,7 +762,9 @@ func ReceiverHtlcSpendTimeout(signer Signer, signDesc *SignDescriptor, func SecondLevelHtlcScript(revocationKey, delayKey *btcec.PublicKey, csvDelay uint32) ([]byte, error) { - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + ToLocalScriptSize, + )) // If this is the revocation clause for this script is to be executed, // the spender will push a 1, forcing us to hit the true clause of this @@ -815,7 +832,9 @@ func SecondLevelHtlcScript(revocationKey, delayKey *btcec.PublicKey, func LeaseSecondLevelHtlcScript(revocationKey, delayKey *btcec.PublicKey, csvDelay, cltvExpiry uint32) ([]byte, error) { - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + ToLocalScriptSize + LeaseWitnessScriptSizeOverhead, + )) // If this is the revocation clause for this script is to be executed, // the spender will push a 1, forcing us to hit the true clause of this @@ -998,7 +1017,9 @@ func CommitScriptToSelf(csvTimeout uint32, selfKey, revokeKey *btcec.PublicKey) // have divulged the revocation hash, allowing them to homomorphically // derive the proper private key which corresponds to the revoke public // key. - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + ToLocalScriptSize, + )) builder.AddOp(txscript.OP_IF) @@ -1054,7 +1075,9 @@ func LeaseCommitScriptToSelf(selfKey, revokeKey *btcec.PublicKey, // have divulged the revocation hash, allowing them to homomorphically // derive the proper private key which corresponds to the revoke public // key. - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + ToLocalScriptSize + LeaseWitnessScriptSizeOverhead, + )) builder.AddOp(txscript.OP_IF) @@ -1199,7 +1222,9 @@ func CommitSpendNoDelay(signer Signer, signDesc *SignDescriptor, // p2wkh output spendable immediately, requiring no contestation period. func CommitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { // This script goes to the "other" party, and is spendable immediately. - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + P2WPKHSize, + )) builder.AddOp(txscript.OP_0) builder.AddData(btcutil.Hash160(key.SerializeCompressed())) @@ -1219,7 +1244,9 @@ func CommitScriptUnencumbered(key *btcec.PublicKey) ([]byte, error) { // OP_CHECKSIGVERIFY // 1 OP_CHECKSEQUENCEVERIFY func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + ToRemoteConfirmedScriptSize, + )) // Only the given key can spend the output. builder.AddData(key.SerializeCompressed()) @@ -1248,7 +1275,7 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) { func LeaseCommitScriptToRemoteConfirmed(key *btcec.PublicKey, leaseExpiry uint32) ([]byte, error) { - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize(45)) // Only the given key can spend the output. builder.AddData(key.SerializeCompressed()) @@ -1310,7 +1337,9 @@ func CommitSpendToRemoteConfirmed(signer Signer, signDesc *SignDescriptor, // OP_16 OP_CSV // OP_ENDIF func CommitScriptAnchor(key *btcec.PublicKey) ([]byte, error) { - builder := txscript.NewScriptBuilder() + builder := txscript.NewScriptBuilder(txscript.WithScriptAllocSize( + AnchorScriptSize, + )) // Spend immediately with key. builder.AddData(key.SerializeCompressed()) diff --git a/input/script_utils_test.go b/input/script_utils_test.go index 41f004a4b3..05bc6b5d13 100644 --- a/input/script_utils_test.go +++ b/input/script_utils_test.go @@ -2034,3 +2034,215 @@ func TestSpecificationKeyDerivation(t *testing.T) { actualRevocationPrivKeyHex) } } + +// BenchmarkScriptBuilderAlloc benchmarks the script builder's default +// allocation. +func BenchmarkScriptBuilderAlloc(t *testing.B) { + dummyData := []byte("dummy data") + randomPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + randomPub := randomPriv.PubKey() + randomPubBytes := randomPub.SerializeCompressed() + + t.ReportAllocs() + t.ResetTimer() + + // We run each iteration 1000 times to make the script allocation stand + // out a bit more vs. the overhead of the rest of the function calls. + for i := 0; i < t.N*1000; i++ { + err := runScriptAllocTest(dummyData, randomPubBytes, randomPub) + if err != nil { + t.Fatal(err) + } + } +} + +// TestScriptBuilderAlloc makes sure the scripts generated by the utils have the +// correct length. +func TestScriptBuilderAlloc(t *testing.T) { + t.Parallel() + + dummyData := []byte("dummy data") + randomPriv, err := btcec.NewPrivateKey() + require.NoError(t, err) + randomPub := randomPriv.PubKey() + randomPubBytes := randomPub.SerializeCompressed() + + err = runScriptAllocTest(dummyData, randomPubBytes, randomPub) + require.NoError(t, err) +} + +// runScriptAllocTest runs the script size test. We don't use the "require" +// library to assert the length to not influence the benchmark too much. +func runScriptAllocTest(dummyData, randomPubBytes []byte, + randomPub *btcec.PublicKey) error { + + const ( + maxCLTV = 500000000 + maxCSV = (1 << 31) - 1 + ) + script, err := WitnessScriptHash(dummyData) + if err != nil { + return err + } + if len(script) != P2WSHSize { + return fmt.Errorf("expected script size of %d", P2WSHSize) + } + + script, err = WitnessPubKeyHash(randomPubBytes) + if err != nil { + return err + } + if len(script) != P2WPKHSize { + return fmt.Errorf("expected script size of %d", P2WPKHSize) + } + + script, err = GenerateP2SH(dummyData) + if err != nil { + return err + } + if len(script) != NestedP2WPKHSize { + return fmt.Errorf("expected script size of %d", + NestedP2WPKHSize) + } + + script, err = GenerateP2PKH(dummyData) + if err != nil { + return err + } + if len(script) != P2PKHSize { + return fmt.Errorf("expected script size of %d", P2PKHSize) + } + + script, err = GenMultiSigScript(randomPubBytes, randomPubBytes) + if err != nil { + return err + } + if len(script) != MultiSigSize { + return fmt.Errorf("expected script size of %d", MultiSigSize) + } + + script, err = SenderHTLCScript( + randomPub, randomPub, randomPub, dummyData, false, + ) + if err != nil { + return err + } + if len(script) != OfferedHtlcScriptSize { + return fmt.Errorf("expected script size of %d", + OfferedHtlcScriptSize) + } + + script, err = SenderHTLCScript( + randomPub, randomPub, randomPub, dummyData, true, + ) + if err != nil { + return err + } + if len(script) != OfferedHtlcScriptSizeConfirmed { + return fmt.Errorf("expected script size of %d", + OfferedHtlcScriptSizeConfirmed) + } + + script, err = ReceiverHTLCScript( + maxCLTV, randomPub, randomPub, randomPub, dummyData, false, + ) + if err != nil { + return err + } + if len(script) != AcceptedHtlcScriptSize { + return fmt.Errorf("expected script size of %d", + AcceptedHtlcScriptSize) + } + + script, err = ReceiverHTLCScript( + maxCLTV, randomPub, randomPub, randomPub, dummyData, true, + ) + if err != nil { + return err + } + if len(script) != AcceptedHtlcScriptSizeConfirmed { + return fmt.Errorf("expected script size of %d", + AcceptedHtlcScriptSizeConfirmed) + } + + script, err = SecondLevelHtlcScript(randomPub, randomPub, maxCSV) + if err != nil { + return err + } + if len(script) != ToLocalScriptSize { + return fmt.Errorf("expected script size of %d", + ToLocalScriptSize) + } + + script, err = LeaseSecondLevelHtlcScript( + randomPub, randomPub, maxCSV, maxCLTV, + ) + if err != nil { + return err + } + if len(script) != ToLocalScriptSize+LeaseWitnessScriptSizeOverhead { + return fmt.Errorf("expected script size of %d", + ToLocalScriptSize+LeaseWitnessScriptSizeOverhead) + } + + script, err = CommitScriptToSelf(maxCSV, randomPub, randomPub) + if err != nil { + return err + } + if len(script) != ToLocalScriptSize { + return fmt.Errorf("expected script size of %d", + ToLocalScriptSize) + } + + script, err = LeaseCommitScriptToSelf( + randomPub, randomPub, maxCSV, maxCLTV, + ) + if err != nil { + return err + } + if len(script) != ToLocalScriptSize+LeaseWitnessScriptSizeOverhead { + return fmt.Errorf("expected script size of %d", + ToLocalScriptSize+LeaseWitnessScriptSizeOverhead) + } + + script, err = CommitScriptUnencumbered(randomPub) + if err != nil { + return err + } + if len(script) != P2WPKHSize { + return fmt.Errorf("expected script size of %d", P2WPKHSize) + } + + script, err = CommitScriptToRemoteConfirmed(randomPub) + if err != nil { + return err + } + if len(script) != ToRemoteConfirmedScriptSize { + return fmt.Errorf("expected script size of %d", + ToRemoteConfirmedScriptSize) + } + + script, err = LeaseCommitScriptToRemoteConfirmed(randomPub, maxCSV) + if err != nil { + return err + } + if len(script) != + ToRemoteConfirmedScriptSize+LeaseWitnessScriptSizeOverhead { + + return fmt.Errorf("expected script size of %d", + ToRemoteConfirmedScriptSize+ + LeaseWitnessScriptSizeOverhead) + } + + script, err = CommitScriptAnchor(randomPub) + if err != nil { + return err + } + if len(script) != AnchorScriptSize { + return fmt.Errorf("expected script size of %d", + AnchorScriptSize) + } + + return nil +}