From e36a2510fcc843ff401617ea2d71e599dd225f25 Mon Sep 17 00:00:00 2001 From: Arkadiusz Osowski Date: Mon, 8 Jul 2024 14:50:01 +0200 Subject: [PATCH 1/2] ARCO-155: separate tx validators --- internal/beef/bump_validation.go | 125 ---- internal/validator/beef/beef_validator.go | 204 +++++++ .../beef/beef_validator_test.go} | 86 ++- .../validator/default/default_validator.go | 302 +--------- .../default/default_validator_test.go | 538 +----------------- internal/validator/validation.go | 237 ++++++++ internal/validator/validation_test.go | 397 +++++++++++++ internal/validator/validator.go | 59 +- pkg/api/handler/default.go | 54 +- 9 files changed, 1041 insertions(+), 961 deletions(-) delete mode 100644 internal/beef/bump_validation.go create mode 100644 internal/validator/beef/beef_validator.go rename internal/{beef/bump_validation_test.go => validator/beef/beef_validator_test.go} (72%) create mode 100644 internal/validator/validation.go create mode 100644 internal/validator/validation_test.go diff --git a/internal/beef/bump_validation.go b/internal/beef/bump_validation.go deleted file mode 100644 index e941016cc..000000000 --- a/internal/beef/bump_validation.go +++ /dev/null @@ -1,125 +0,0 @@ -package beef - -import ( - "errors" - "fmt" - - "github.com/libsv/go-bc" - "github.com/libsv/go-bt/v2" - "github.com/libsv/go-bt/v2/bscript/interpreter" -) - -func CalculateInputsOutputsSatoshis(tx *bt.Tx, inputTxs []*TxData) (uint64, uint64, error) { - inputSum := uint64(0) - - for _, input := range tx.Inputs { - inputParentTx := findParentForInput(input, inputTxs) - - if inputParentTx == nil { - return 0, 0, errors.New("invalid parent transactions, no matching trasactions for input") - } - - inputSum += inputParentTx.Transaction.Outputs[input.PreviousTxOutIndex].Satoshis - } - - outputSum := tx.TotalOutputSatoshis() - - return inputSum, outputSum, nil -} - -func ValidateScripts(tx *bt.Tx, inputTxs []*TxData) error { - for i, input := range tx.Inputs { - inputParentTx := findParentForInput(input, inputTxs) - if inputParentTx == nil { - return errors.New("invalid parent transactions, no matching trasactions for input") - } - - err := verifyScripts(tx, inputParentTx.Transaction, i) - if err != nil { - return errors.New("invalid script") - } - } - - return nil -} - -func verifyScripts(tx, prevTx *bt.Tx, inputIdx int) error { - input := tx.InputIdx(inputIdx) - prevOutput := prevTx.OutputIdx(int(input.PreviousTxOutIndex)) - - err := interpreter.NewEngine().Execute( - interpreter.WithTx(tx, inputIdx, prevOutput), - interpreter.WithForkID(), - interpreter.WithAfterGenesis(), - ) - - return err -} - -func EnsureAncestorsArePresentInBump(tx *bt.Tx, beefTx *BEEF) error { - minedAncestors := make([]*TxData, 0) - - for _, input := range tx.Inputs { - if err := findMinedAncestorsForInput(input, beefTx.Transactions, &minedAncestors); err != nil { - return err - } - } - - for _, tx := range minedAncestors { - if !existsInBumps(tx, beefTx.BUMPs) { - return errors.New("invalid BUMP - input mined ancestor is not present in BUMPs") - } - } - - return nil -} - -func findMinedAncestorsForInput(input *bt.Input, ancestors []*TxData, minedAncestors *[]*TxData) error { - parent := findParentForInput(input, ancestors) - if parent == nil { - return fmt.Errorf("invalid BUMP - cannot find mined parent for input %s", input.String()) - } - - if parent.IsMined() { - *minedAncestors = append(*minedAncestors, parent) - return nil - } - - for _, in := range parent.Transaction.Inputs { - err := findMinedAncestorsForInput(in, ancestors, minedAncestors) - if err != nil { - return err - } - } - - return nil -} - -func findParentForInput(input *bt.Input, parentTxs []*TxData) *TxData { - parentID := input.PreviousTxIDStr() - - for _, ptx := range parentTxs { - if ptx.GetTxID() == parentID { - return ptx - } - } - - return nil -} - -func existsInBumps(tx *TxData, bumps []*bc.BUMP) bool { - bumpIdx := int(*tx.BumpIndex) - txID := tx.GetTxID() - - if len(bumps) > bumpIdx { - leafs := bumps[bumpIdx].Path[0] - - for _, lf := range leafs { - if txID == *lf.Hash { - return true - } - } - } - - return false -} diff --git a/internal/validator/beef/beef_validator.go b/internal/validator/beef/beef_validator.go new file mode 100644 index 000000000..e9a479bee --- /dev/null +++ b/internal/validator/beef/beef_validator.go @@ -0,0 +1,204 @@ +package beef + +import ( + "context" + "errors" + "fmt" + + "github.com/bitcoin-sv/arc/internal/beef" + "github.com/bitcoin-sv/arc/internal/validator" + "github.com/bitcoin-sv/arc/pkg/api" + "github.com/libsv/go-bc" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript/interpreter" + "github.com/ordishs/go-bitcoin" +) + +type BeefValidator struct { + policy *bitcoin.Settings +} + +func New(policy *bitcoin.Settings) *BeefValidator { + return &BeefValidator{ + policy: policy, + } +} + +func (v *BeefValidator) ValidateTransaction(ctx context.Context, beefTx *beef.BEEF, feeValidation validator.FeeValidation, scriptValidation validator.ScriptValidation) (*bt.Tx, error) { + feeQuote := api.FeesToBtFeeQuote(v.policy.MinMiningTxFee) + + for _, btx := range beefTx.Transactions { + // verify only unmined transactions + if btx.IsMined() { + continue + } + + tx := btx.Transaction + + if err := validator.CommonValidateTransaction(v.policy, tx); err != nil { + return tx, err + } + + if feeValidation == validator.StandardFeeValidation { + if err := standardCheckFees(tx, beefTx, feeQuote); err != nil { + return tx, err + } + } + + if scriptValidation == validator.StandardScriptValidation { + if err := validateScripts(tx, beefTx.Transactions); err != nil { + return tx, err + } + } + } + + if err := ensureAncestorsArePresentInBump(beefTx.GetLatestTx(), beefTx); err != nil { + return beefTx.GetLatestTx(), validator.NewError(err, api.ErrStatusMinedAncestorsNotFound) + } + + // TODO: verify merkle roots + + return nil, nil +} + +func standardCheckFees(tx *bt.Tx, beefTx *beef.BEEF, feeQuote *bt.FeeQuote) error { + expectedFees, err := validator.CalculateMiningFeesRequired(tx.SizeWithTypes(), feeQuote) + if err != nil { + return validator.NewError(err, api.ErrStatusFees) + } + + inputSatoshis, outputSatoshis, err := calculateInputsOutputsSatoshis(tx, beefTx.Transactions) + if err != nil { + return validator.NewError(err, api.ErrStatusFees) + } + + actualFeePaid := inputSatoshis - outputSatoshis + + if inputSatoshis < outputSatoshis { + // force an error without wrong negative values + actualFeePaid = 0 + } + + if actualFeePaid < expectedFees { + err := fmt.Errorf("transaction fee of %d sat is too low - minimum expected fee is %d sat", actualFeePaid, expectedFees) + return validator.NewError(err, api.ErrStatusFees) + } + + return nil +} + +func calculateInputsOutputsSatoshis(tx *bt.Tx, inputTxs []*beef.TxData) (uint64, uint64, error) { + inputSum := uint64(0) + + for _, input := range tx.Inputs { + inputParentTx := findParentForInput(input, inputTxs) + + if inputParentTx == nil { + return 0, 0, errors.New("invalid parent transactions, no matching trasactions for input") + } + + inputSum += inputParentTx.Transaction.Outputs[input.PreviousTxOutIndex].Satoshis + } + + outputSum := tx.TotalOutputSatoshis() + + return inputSum, outputSum, nil +} + +func validateScripts(tx *bt.Tx, inputTxs []*beef.TxData) error { + for i, input := range tx.Inputs { + inputParentTx := findParentForInput(input, inputTxs) + if inputParentTx == nil { + return validator.NewError(errors.New("invalid parent transactions, no matching trasactions for input"), api.ErrStatusUnlockingScripts) + } + + err := checkScripts(tx, inputParentTx.Transaction, i) + if err != nil { + return validator.NewError(errors.New("invalid script"), api.ErrStatusUnlockingScripts) + } + } + + return nil +} + +// TODO move to common +func checkScripts(tx, prevTx *bt.Tx, inputIdx int) error { + input := tx.InputIdx(inputIdx) + prevOutput := prevTx.OutputIdx(int(input.PreviousTxOutIndex)) + + err := interpreter.NewEngine().Execute( + interpreter.WithTx(tx, inputIdx, prevOutput), + interpreter.WithForkID(), + interpreter.WithAfterGenesis(), + ) + + return err +} + +func ensureAncestorsArePresentInBump(tx *bt.Tx, beefTx *beef.BEEF) error { + minedAncestors := make([]*beef.TxData, 0) + + for _, input := range tx.Inputs { + if err := findMinedAncestorsForInput(input, beefTx.Transactions, &minedAncestors); err != nil { + return err + } + } + + for _, tx := range minedAncestors { + if !existsInBumps(tx, beefTx.BUMPs) { + return errors.New("invalid BUMP - input mined ancestor is not present in BUMPs") + } + } + + return nil +} + +func findMinedAncestorsForInput(input *bt.Input, ancestors []*beef.TxData, minedAncestors *[]*beef.TxData) error { + parent := findParentForInput(input, ancestors) + if parent == nil { + return fmt.Errorf("invalid BUMP - cannot find mined parent for input %s", input.String()) + } + + if parent.IsMined() { + *minedAncestors = append(*minedAncestors, parent) + return nil + } + + for _, in := range parent.Transaction.Inputs { + err := findMinedAncestorsForInput(in, ancestors, minedAncestors) + if err != nil { + return err + } + } + + return nil +} + +func findParentForInput(input *bt.Input, parentTxs []*beef.TxData) *beef.TxData { + parentID := input.PreviousTxIDStr() + + for _, ptx := range parentTxs { + if ptx.GetTxID() == parentID { + return ptx + } + } + + return nil +} + +func existsInBumps(tx *beef.TxData, bumps []*bc.BUMP) bool { + bumpIdx := int(*tx.BumpIndex) + txID := tx.GetTxID() + + if len(bumps) > bumpIdx { + leafs := bumps[bumpIdx].Path[0] + + for _, lf := range leafs { + if txID == *lf.Hash { + return true + } + } + } + + return false +} diff --git a/internal/beef/bump_validation_test.go b/internal/validator/beef/beef_validator_test.go similarity index 72% rename from internal/beef/bump_validation_test.go rename to internal/validator/beef/beef_validator_test.go index d01476bf3..407d722f3 100644 --- a/internal/beef/bump_validation_test.go +++ b/internal/validator/beef/beef_validator_test.go @@ -1,14 +1,77 @@ package beef import ( + "context" "encoding/hex" + "encoding/json" "errors" "testing" + "github.com/bitcoin-sv/arc/internal/beef" + "github.com/bitcoin-sv/arc/internal/testdata" + validation "github.com/bitcoin-sv/arc/internal/validator" + "github.com/bitcoin-sv/arc/pkg/api" + "github.com/ordishs/go-bitcoin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestBeefValidator(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + beefStr string + expectedErr error + expectedErrTxID string + }{ + { + name: "valid fully mined beef", + beefStr: "0100beef04fe4c7c0c000e02fd460302d2e6252f996ab1a6fcbe8911e8f865bb719c0e11787397fd818b5bb1ff554a3cfd470300d67d0a32df0ecd5976479f068f19ff671e0a285570b915bc6e6658e98a9c0e3401fda20100eafe36c6f1adef584b1d199286c5024a4791eb16f35161c4b69cc119c1c9493c01d00077605d020124b1f6e039f1b2ccebf3e46acf45584bc559b654b57179b99d906a0169002c4fe7d1d2b2990df28e5132b059e10cc6ce9be85a79f834546eee881a8f968d013500a187befab2d1cc22004f96045afecd929e2edc7017a044aaf0f530847afa1644011b00bf5108b8176d4abba8c80f935607abc6a6bc38b9461c86a812369975ad2abb1e010c002efe8c4f9a630b0d0f4a410d93523d7003085dd166248d6e76f405fe8fc606720107007043c5b1be3e6952859bca6d6320f5a0f6c5bba5d5fbfd8a195f1d9ec4e0964c010200822378b29fd273a3bf38a4b852089e8f5a1251b5fedeee6ee2f548c0ee93a14e010000788ff2bf42a41c2e32bd59504fa59fd8b2ab0058430f9d4d2988ee8341ccac1b010100470c98a5fb48ed000ca746a05c7d398d2b76efe70a65c67c1cec14a8c8c84cce010100de821cfed42c83ab4dfc3fef452ec5f4086d18fb7324c046826cd51e0fea2c8b0101006422ad7eb4999dc4ca89a3488862f03b4101c32ed49c19e3df7563958e6e480201010087ce3715a94573ac7746f3b66e4070d313b4a8df6114509678ab08c425250b06fe137c0c000e04fde00a02827f1758c64b4a0b1226c54316ddeed4618500f026602ebca3cd4b96174a690afde10a001a906867a2cbd98095453400107f814e5380640443bf4504eb6fe412acba674bfdac0d001f3da43722e72c749846913dbce0623b1d22fa8499bbca49f3f9aec4cd5b3d70fdad0d02213e4fca3103f812ffcba253caf452c6811947ff6f2fb99b4e18baa1233e84b602fd7105001399fab1150a4f8013dca738bec4ae8c5b215e301697d6453f648230b36cd431fdd70600282c33fe4ec70f91818781daa6a97e85022bc2031a370202a385f2aeb9086a1a02fdb90200fd35a389307434d05891122dc81f3a5246f577334de368ebcfc15836648e73a8fd6a0300ed1732da6c0666a7270d63ca0d4af8ba1c9a80c149bccd5e1ac039159418740a02fd5d010019dc6865859cc67245c5d9b04bc3a9141a4e6fda17ba33d24271600244b2dd35fdb40100ccb07e295f3bbac75957015c58c5f7f64be4904865c95ebf4181c40d0089429602af002d31340de572c9cdbe9c75c62474ad3130e417d7a61b370aab7345901f69badfdb006c6b6e5958eb5f2ae497e4b7d8ecc90ac614856ada2458f35e1f2880fd155530025600efb95e533fbed11730d7eb9f2c891a885cb658f4ca058fdf6bed8d348c9415de6c0056e17aded7bd785e6a44cf023a294440bd89886dfa5d48ce28386199361e4b47022a003c5a68cac12c161d6a3c5a1e8ba77d9df937954a3c5ebb669fc5d3f04883c22237003978958e578f7326e8271b058ad60c85f1d2d404bdf75611ef9192de27ce644d021400692e79f2246540d6526e8261bda1b430f218be172a2672ae0a58e5fe20c359331a00595cbb11c5591f7630c31a779fe71b5a48526b525078a37492b087dcff4f1314020b00d6a30d5bccc4f3d9387e56e9e1f32de9db91c52c8fb4179aab7732a38df8df600c009fe2e3698677771d5fd4819796ee8342277ae3bdcd68e4e38ad3c04d207df06d0204007635530acf3aff406ca2ae825aa7253c3c1fb92cadf0124145a4565477c9198f0700f1f84a4c5efaeb4d5660968aa9234db2a98c5bca3f5942ec1d007dc2d996822f0202003578890ece8c5028080f613c01b8662df60c4c5ea6cc858e35ec266f3e8c8bf80300d2fcdb70b5967e0b53341b6bcc517511aa977ec6c98e587035e734bafb2df40201000050e8890913d743a6a49e244dab275bcafb7ec2fd8828163e6a07560351b77b2e01010039787d0d5252e1347883ebcad07c141448e366d2b40d3241211482ef52f34d97010100278ecd209c80ea49e6581c8b9e03b118e66cbc39e4acb14b26bda7d378fe2117feeb7b0c000f02fdd438020dacf934645c462a155ca35453ab578e7d510687fda689a565e000fc4df11cd5fdd53800de97bf2bdde019a90f6c784c3731b71a45ade39a58c436e2c70a4c9f83371e1d01fd6b1c00aaccb26fe28312165b6a2fd6414c651dd178a8053324d25008b8f533af7ad30a01fd340e0010cd00845ba8b3e5e08a057de308885b01ec1cb2f4cc56da6583ff1f3f9d98ff01fd1b0700d712139ba223278e95809bd1aebd6d334288ef66b989116a344485c603e9998d01fd8c0300097e0e68b5f5aab8ef48a8983b4b4fa992d80bd4cc2fd2c3069be71407d73b0001fdc701008be1cc85f6bb068b86ad60cfbd321878c2774c99274e953530cf4d7611227f3b01e2000453717a6dee5b33d9c379e7b262d17ddcd6267b159fd8a0b980603d03ce31b601700050992dd58d2332d87df4874852f9275eeb9c7813e1a6c1e1cad1a74a9e5560a10139003fdc83bc63d0af296cd6a71e45355eab21634d829f6b17e769fe0d2308a99484011d001eb98912db04c69f675a4cfb64a57167d9b136d174c0d0e1f2635840a27a2819010f00b0a43e31d356c1ccd7cd1733f8bf5885013361d7f02627629136b360c55aa43a010600a7b2280eb6bc6f398f7f5423033bd011e819caf0beeb248f26a1498fbc7e4bb1010200152e881046cdb491d33c93d799f28e07686708db2cdc8a3011c375f7b9bef87a010000851add544d85300e318ec60be010f38f2bea85cc86ce5a972df28b94e53f5801010100a7308e947ed378dcf6e6f79ba0bb8d062f7c7ffcc025e5d08fc30a3288e87ef5fe1c7c0c000e02fd5a1902e5232220aee5069017d31cc30818bcc971de3e6418f6e62b8cc9a3430d64f3e9fd5b19004359d0c4ca1b47399960cb57ff4b0d2c850c825ba56a2119c3ff875fece11a4001fdac0c003c0fd4b7a8621c97bf7b6adad9569fe1f07a52c41d472d1e3407f9e37b3400d701fd5706006400dbcd6c1ea8a5c8cccfa153eb02076bf03b4ac7fef38db8e3a5032d908f3101fd2a0300ceac7150203519610ec78f93d64bbdc2e76e9cfadb72a83070d5fc72c8c0291a01fd94010058dde4e9d746faed13b29bd0f44ea86cdab712bc3d430a88e8cec22364e19cfd01cb001f77543e1a81dc8d4fdef4a4b132cf907b8a22de393400035a40391904fcb5c20164002cc86783ce17e19b7a56e0d08ba4c5d94cfd586771dd28e2fe1284d887db1285013300ceb2afce84f3e257bce737ebaef9ded331348d750d17f408c22d79dc8e9de9e9011800d939fb970c2044768981bea25d19be80302773a4b0d469566315258d39db5059010d00c815d2c629a1d419d6ef3c33a670da1bc64f7f23a840365fc292aab7d7b4f770010700828ced5731867ba622104fe64bd18a2132097623f8fe4a834537b329dbb6d06a010200f9df4e876faa4ddd9f909bfafed085ad24297f80003a3700bd13250498e252190100004834f3f8c1be8c4f43bb61dd6d0eea6ab996bae149aefd5f9aced05c54b3395901010036679c2ca45cd1d37b4b0d1d4a9a0e923d80f6241957fd61969b641ca1a2b62c060100000001aa5df6cc8e5f41e8770d222d06d2dfdea647769871044df714c99b59d3528858000000006b483045022100d65cb97677ba806e752d63ea988faf5fb00cabd0df78bb78fe9f42a079e7ae540220158a2d51ca8b03f18691eab1c4fa5e85b6f21846df9bfaf797c995b1c9f34e47412103597c97b42c5f880e27eaf2eb2de0fb0fe5a81d689c646b2c6527acbc9c2509c7ffffffff0264000000000000001976a914e5c8e12ec010e7bf318ebc94efcb887ff751069c88ac8e010000000000001976a914ae791b7e9d0108b4000ac4138c2b47d8cfea404288ac0000000001010100000001a25525c35101ad7ad7653e16d5d525762f3826e4c7b87077a096aebabe292ad9000000006b4830450221009fa60fe23a004850fd671a834534b278d4a2a9a4e7d8dfc0e4f9ffcb2899c1ef022010a8e718b608dfd308cdbc382e6f666583c8148b5f70819fbb7df1f1edd8cc164121023d72bbb613c7e8a4230c080ffaff1abc038df9008046b2157f888f36f427d2f0ffffffff0228000000000000001976a914596ce4dbbefd792d782ccb7ce5652726fd0c676288acca010000000000001976a9143e8a2536d8bc8f2a995800f16e068ee85aa1939288ac00000000010301000000017a7123cf30268df225315c47b6ccbfd3e4f5c19f765bb9abc8fbeed134eb0b7f010000006a47304402201715f13c3456310f7b73386ca947621b53f71f27d2a23c0d1f09deb4b8931ca7022000d9c8daa400b1c2ad34df4c22e024e0d9191ecefe83160f47fbf53026fb4d53412102b6f13a7f2599f7312cf55ca1524bf9ebefeed22daa1f80ffed82df038db0167affffffff012d000000000000001976a9148e4221cdcf3f547ffd0aee46411b40a3248fdb3688ac0000000001020100000001a25525c35101ad7ad7653e16d5d525762f3826e4c7b87077a096aebabe292ad9010000006a47304402202ed74a7c67dd15fa9594ae600775e3a289511ceda7994b7914bd94435a269c7202205767d4deaf8db2c7c3a862a2b0e8f43f377a597bbbbed10409819bc2239064804121024ffdb4c74d0f3e92dbfe41348ee5295a8a8aa91f7eea1c17c879baf3d31492c3ffffffff01e7030000000000001976a914424cc7a221a4bf81790a850014a5e1a1663d3b5888ac0000000001010100000001213e4fca3103f812ffcba253caf452c6811947ff6f2fb99b4e18baa1233e84b6000000006a47304402202b577840e0fbab66a150a79feeb9ea2e0469df83f44a4aebb4779c35ea1d7b5402204764d20a211b112e8907a3a80e418dcc933ba4e49dea7072bd5814cca8a76e664121027db94b451dafb7368da30ed73abbf064a622ebcffc1bd3362802fd8eaaf5fc6dffffffff0163000000000000001976a9144451354c29626cbeb4f130e6bb7ddd54151c6b3b88ac0000000001000100000005d2e6252f996ab1a6fcbe8911e8f865bb719c0e11787397fd818b5bb1ff554a3c000000006a4730440220179e09c88dc5d83e6db5abcf2a46c989ba766612f3be664f66f9d5932681b42e02207ea73be5317362c511e76a40f3b416ca2ec99ce891bd55fbbfc60f3e2598c1be41210277e25e8e4ab96e46d94a037411d195b537810b1d7301c2d72e9eb40bb47aae34ffffffff827f1758c64b4a0b1226c54316ddeed4618500f026602ebca3cd4b96174a690a000000006b4830450221008a9418e0be1b65b45ac27449071bf8a7cb55d9008ae5c8d28da47bce9bc2cc28022056aa8ffd170eb4b86b2be7da81b1d01dd0f026d0488645c3b27f6e521eebb51a412103138a3aac623a5fc9789cd9476efad0605dea61f3e7cd5089195eb0b446b6ab4effffffff0dacf934645c462a155ca35453ab578e7d510687fda689a565e000fc4df11cd5000000006a4730440220147729079aef1449d0c1b08f79c1fa4749d685190d0fdd25801725845d0186740220796be61c7a059e65b4e418d1fec99a596ea565b179683d334dc7a2e2312ed466412103ba49c495254796f5ccf0e28f205f62965fafc33367b2b8d6609e5de30c206ad4ffffffff213e4fca3103f812ffcba253caf452c6811947ff6f2fb99b4e18baa1233e84b6010000006b483045022100e5374c185ffb992bcabec480572427f402911cdaeda92246f2a94813a32f33ab0220676121e6bd049c981593a8093f779927cc95bdf56d50069b7786ad9b9fe528c84121035d1d732dbe247c0886753c84dc3d2fc96a9eac26662e8664fe9ce8f67ab6dd98ffffffffe5232220aee5069017d31cc30818bcc971de3e6418f6e62b8cc9a3430d64f3e9010000006a473044022048e3532181d848dcf69a4da9b8ae296b90b4660a27f880b928ce15e9d3c5979e02205cef48653b1defbc2a87d93b268693c8d078199b69eb5bba32ba10f301727bf641210343caa07997898400cefe7a28445b233d30463d13359c1d87ac42ea5da61432a0ffffffff01ce070000000000001976a9145c21ae83ea5892dea33b9b002eb1d8450b581ea888ac0000000000", + expectedErr: nil, + expectedErrTxID: "", + }, + { + name: "valid not mined beef", + beefStr: "0100beef03fe4e6d0c001002fd9c67028ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160cfd9d6700db1332728830a58c83a5970dcd111a575a585b43b0492361ea8082f41668f8bd01fdcf3300e568706954aae516ef6df7b5db7828771a1f3fcf1b6d65389ec8be8c46057a3c01fde6190001a6028d13cc988f55c8765e3ffcdcfc7d5185a8ebd68709c0adbe37b528557b01fdf20c001cc64f09a217e1971cabe751b925f246e3c2a8e145c49be7b831eaea3e064d7501fd7806009ccf122626a20cdb054877ef3f8ae2d0503bb7a8704fdb6295b3001b5e8876a101fd3d0300aeea966733175ff60b55bc77edcb83c0fce3453329f51195e5cbc7a874ee47ad01fd9f0100f67f50b53d73ffd6e84c02ee1903074b9a5b2ac64c508f7f26349b73cca9d7e901ce006ce74c7beed0c61c50dda8b578f0c0dc5a393e1f8758af2fb65edf483afcaa68016600e32475e17bdd141d62524d0005989dd1db6ca92c6af70791b0e4802be4c5c8c1013200b88162f494f26cc3a1a4a7dcf2829a295064e93b3dbb2f72e21a73522869277a011800a938d3f80dd25b6a3a80e450403bf7d62a1068e2e4b13f0656c83f764c55bb77010d006feac6e4fea41c37c508b5bfdc00d582f6e462e6754b338c95b448df37bd342c010700bf5448356be23b2b9afe53d00cee047065bbc16d0bbcc5f80aa8c1b509e45678010200c2e37431a437ee311a737aecd3caae1213db353847f33792fd539e380bdb4d440100005d5aef298770e2702448af2ce014f8bfcded5896df5006a44b5f1b6020007aeb01010091484f513003fcdb25f336b9b56dafcb05fbc739593ab573a2c6516b344ca532fede850c000d02fd860c00661459da8afa777590dc3c736af35ded5fc51b602688a3a7c93edaebe6a4652bfd870c029fc71a2f88088174c4d0e0348e7087c9b355a4e93e5b2ec4a5f41fdd69272f8601fd420600a5a336c4022e226e5695521b7ff33ae86afd0f1eeb17ee0dc62501f385fc5b0901fd20030018013a8fbb3176ba39c193948c2c57cec747ec73aa362f52f0720c3391a7c66701fd91010014d30a290a1b63e7e6e16f8e09e2e93a71042462451e1f0b2f9ec8971e0c736a01c900421970e542d08b8b52eb3ba45541226d885dcad9dbe7ccec4111c9cd4c54acb30165005ae426e8c77d1b1153b2425b935ede63f1b98e7852e9eee493415e7bcf2ec56301330039eb742b6167dcb3d9f680e60d13af943f6fff7f6a84dc6ac5680eb32c51e728011800d47cdf86aa745c735cb1de8b5c72dc43d927c58500b76e108b86556845c3aa53010d00725cc42f64c415e1d17803764a441ece296509ecdc77e5962141641df91e276b0107007dd3c756b59f354d81efe05248242d603e4baf5a568b34cfa84b1152c65d2b440102001083fd215308c82b36ab82b7aaf8ef5016edd1f7fe3d7cd0d7c7026d17d6841c010000848831afed0a0552896bf420ad66647af4d0dc8c76d19a6e912a1987ac925dba0101004d4fe46bfd107d575616f76b860ab00906e6ac47250d776a4c2cc67345eef4e8fef3850c000b04fde00502f8aea749791d0907e46bbb7df82dc52f84a9b83434e563e3a6496b95239fc08dfde1050076c8b774bf78f74252cee9698b49fccf87937ae4f2b01b7a87f23b060336585dfd26060295e93ce1a376cfd842ffbd849f0ee37330d6b62a51cddfacfbed9c1a4d49cce7fd270600d232986aa95c1db58e9f56fd3caf52cd812428ffc3fdcdf5be21edea66af1e3802fdf10200eef6a321ca9dcd30df8eb9eeaeacdb0a0535e269279e6ed87c6ab5bfdd14f167fd120300f38cdc9cf336f46aa742e3a1ddd3a50418ca5d52fb2d613a49e9d1f0821fde6c02fd790100e1e3d7d90c3859990c6ea7588f05419fab6eebff078edf3f66a6cd8a87faa7c5fd880100b0f4a9b7219d01ca579ad5e6741fdf677c899f1de9855945f13a286c0452e9b202bd007f0f30d4d1ffc36093444b54cb39693d6c34f4810863850adc824733aeaa55e9c500c37b5a7df0dcc18fe9943b9f2927122d2bd3aaaa8c4bac71d9bbecceee1be782025f00b0126c6c3ede9904454251478bd667b1396678a4567bcbcae0fb33f73e0d7aab6300fe3589fe0e5afc3827d35b1065b7aad956021d388fc6ccb1d5d81657d092b1e2022e00bbcd586527ec559f3ecf031064f631b3c372e7b31d1361be3a9b41bd21a09e84300051af813b88ba08d1526fd4418305a0464f499c51ee1e1bc7231ebbf68d33b812021600e822218713eb86174c91b4cc924dbe8c8e2319309b8195e845e94e0b0760ea191900e8ec605b869f93cf6b1e2d74bcbc9aba1be3d9b4f9bd2e82563fd797b8cd4a3e020a000be8607accf5ae93e80c274bc3e045a3d4e8387b3725dbb1e9583a4ed310ef4b0d0084309da5e0385c79ea670be940324a3416ed96e122a24aca17773cb5899b407b02040036bd04fa34848574dc1665ba52bbe49038b9595635fa4b525d7de6ef9c8988210700c4c2ef77377913248dd9fe8e659c6f0f80b9b72fa98ffed670f8fadc5c91f3930202007a3e5caea9497ee7595c8aa9bcb6a08c2d06e215154085f224345aaf3ede4e870300e9bb93416e69b38eacd6aba4258afde7a007ac52dcaa28aabd65aad90ac3bc1b010000c1006f88da5f5841323cd779a19689a43d9311785be06efb22ad98c3cc6be1260901000000029ceda549c273cb093678c42c5765354c735c005cc1feb72e1a291f6bf3b3e312000000006a47304402204c992ff3b4185f9858e72e0b3d01d6d4ed144e363c13afe662c8edf37744e612022018fcd7e2492e365f738fb07fa5c1dfa84ab9f49469559f5540be1e115104066b4121031499e4d192d8e2b2409cb757608bee58701cf6d58d55cff97fa584671bce4728fffffffffc67309c8602eb6a14b805c27641337f114314ac09fa96b2974d73d11997639d010000006a473044022069eeddbc83b657ea6f40d1c6ae535a7993943ce5624494d6069b64fc45d17cef02202346f9131101d94bd15b0fb122f25fbc41e6d0b941b42eb79549c7cfb78e5ac441210242cf3c106badb9695cbf7c64039bce81b0a58cbf89b3a04d90577bc801b2a9efffffffff03e8030000000000001976a914ff98769820cd542ba0c6fc8f0db003ef51a6f84d88ac1f070000000000001976a914e11d3852660e0630189bfd178ed7652fb0be25de88acb8260000000000001976a9141fbd24e6a4b10ce1f04775d0e8e42c4406e3824b88ac00000000010101000000027b0a1b12c7c9e48015e78d3a08a4d62e439387df7e0d7a810ebd4af37661daaa000000006a47304402207d972759afba7c0ffa6cfbbf39a31c2aeede1dae28d8841db56c6dd1197d56a20220076a390948c235ba8e72b8e43a7b4d4119f1a81a77032aa6e7b7a51be5e13845412103f78ec31cf94ca8d75fb1333ad9fc884e2d489422034a1efc9d66a3b72eddca0fffffffff7f36874f858fb43ffcf4f9e3047825619bad0e92d4b9ad4ba5111d1101cbddfe010000006a473044022043f048043d56eb6f75024808b78f18808b7ab45609e4c4c319e3a27f8246fc3002204b67766b62f58bf6f30ea608eaba76b8524ed49f67a90f80ac08a9b96a6922cd41210254a583c1c51a06e10fab79ddf922915da5f5c1791ef87739f40cb68638397248ffffffff03e8030000000000001976a914b08f70bc5010fb026de018f19e7792385a146b4a88acf3010000000000001976a9147d48635f889372c3da12d75ce246c59f4ab907ed88acf7000000000000001976a914b8fbd58685b6920d8f9a8f1b274d8696708b51b088ac00000000010001000000028ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160c000000006b483045022100c5db842085740c8467a6755693e7048b2f4134371a0f01509ee1db9b7274403f022064de309ec9fbdca4ca1ab220cd35d6efd04028217f535cc43649f96250bb53154121028fd1afeee81361e801800afb264e35cdce3037ec6f7dc4f1d1eaba7ad519c948ffffffff9fc71a2f88088174c4d0e0348e7087c9b355a4e93e5b2ec4a5f41fdd69272f86000000006b4830450221009bc74c7331b010f8befcc4a7ee630876282bee47775f04796567dc875ec83c05022064b558766a1f8badc0c26c0b3d7bcde99cf9d774f88a87f8f45d440ca54043a241210336abf8512542305fa13788dae2e4e9f0fb2ae5dc57cb1ced980eaee6e88f343fffffffff02dc050000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88acf3010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac00000000000100000002418eb1b58f47af50a079e1c695e6ce86f79c092caf050128f0d7785381d960d3010000006a47304402207b019f395fdedfe3632433df49eba13e33d8c8f0328834eb0366635853c3e8ff0220505ef2df0702e918897019e49a486ecf8238b179d102cc3c0920564dd6a84d064121034e92b3e62017ee88487a69f8ecda9d17b4daf590d6e06362a4139b1d993dd044fffffffffea6861820d117e799caf1553ce075435f3ea153aff26c9551c71047be4089bb020000006a47304402207b58b596f1094c3e0570f136f1c86ae883a260b6fe671b07ccda85136561bab80220410924b50aa6a7ba9c864f8d206f1bba2c7b384127984e2a1e9db066f82379ca41210301b1d251f89768ff33a01c192a400de69573bdfc44fa67fde13eb259eb81ea64ffffffff03f4010000000000001976a914601d40068f463ce0124e1b18466e1bd7d6b333a388acf4010000000000001976a914668c08cab622e65e3aaa1c1365b8e4c029a6365688acb9260000000000001976a914e861b54724209a7eb9b0512f31c50c1ae2a07cb588ac000000000102010000000195e48d7d5679e0740c85f3a9e1e69a9f5c8493f7ec7c454b0193c15dcc7007cd010000006a47304402205babaf7fb8cc6e9347c0cc743c9f044bf8a3820f05fdc57a3591d4cf2df86bc402202e816d35e9a937cffe8cde4f68ee64b06fc5e070335c0c2262fdf016eaca47b94121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff0264000000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac8e010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac000000000001000000029fc71a2f88088174c4d0e0348e7087c9b355a4e93e5b2ec4a5f41fdd69272f86010000006a47304402203a5c238e8ceccff7b7db05a4dba524f6694599014dbac5ae92dc0c62635c8763022078cab36b04a16eeead80fa49dd4d1c22f14d8084c78977917cfe76fee9d95a84412102993285bd72def003516c414b4fb3c3718d5d003c90f08c75bd6eecc08910585fffffffff396915fa4cff57573f0e80b1d585d5a4f7692984e51577c8ea82e1d88f8404ce060000006a47304402205ba90f2ed3f70c779771185a473e3b1844ad0f8f86bddf39148ad277c9a2298902203df352b710b05501cc3d773e366854770038f9dea65c046b666fd28b124f303d412103c335fc77d28a0181bec42cd7afda547a4d524c5b1f04a7d9d0ec76f1217a0e5affffffff03f4010000000000001976a91494c77286c0552398575ea741337dc6143c7978ec88ac2b050000000000001976a91418d9eaafcf7d8e8ba29ac102ed19b9b3808376a088ac0f4e0000000000001976a914555b7d05b5327c78c7bf7f8748c1b3ce3552168888ac0000000001020100000001f8aea749791d0907e46bbb7df82dc52f84a9b83434e563e3a6496b95239fc08d000000006b483045022100a50ebc1429285edf0d39d77f0ab49e68eaf6e8f97d727671fcc51763235ca39a02200252a185b70fb5ac893ad32e8ffe1e8ade9d3d8993f8db4a8cb27dc6e32c18664121037b231d9fdd2f7a41181de9a14ae11a10b7332b1fdb5526c1a0060eb964f111d6ffffffff0296000000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac5d010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac0000000000010000000183d98243af506e20461097054c6692a09502ee7a99eba67cd20b0d5b66d081f0010000006a473044022055e323fc40f05fdeb836fadf19003d91f08abef97526e04f4c976156faedc83c022050f36b4d23329fa9d6f37bfee952ae3a0bf266573fc19a51ab42595ee674a26a4121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff0264000000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac29010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac000000000001000000032a796907de322523a34e801e304ea43b6938cfa5afe3ff4fea7fbc34bd52164c010000006b483045022100ec1d2ab4bedca3a696a6f0589b65015b07d5a00b7517d03f783050323165b16902200c18cf37ba54454f0e28e610ec14e53be4cfc254182142e111c2571c3432f15d4121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff73bd2e109d1e01d9c5828e15b7e3212e54dd3d9558ceae6514c97e07ad275110010000006b48304502210081d8eb51e97f6c50460a3f526f14f35f8882f51f9455c7b06f63a3aedd16c057022019e8014a1c2bed2b4f9c0e15989056a53400ae3e3ed1114f516fa9b6390af7154121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff95e93ce1a376cfd842ffbd849f0ee37330d6b62a51cddfacfbed9c1a4d49cce7000000006a47304402202b185fd9848a68d76167383535de9d8adcdb2e219e8210641e4aead624532d3e02206f2f24fa29a81f73438a3cf7208bd021fb9fa8c1f30376b00af6bf7766b29d7e41210275aaa422bc13c303b70026b3fd1fff37c199e5e98bd0a429922217f9b861870effffffff024c040000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac2d000000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac0000000000", + expectedErr: nil, + expectedErrTxID: "", + }, + { + name: "invalid beef - low fees", + beefStr: "0100beef01fe4e6d0c001002fd9c67028ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160cfd9d6700db1332728830a58c83a5970dcd111a575a585b43b0492361ea8082f41668f8bd01fdcf3300e568706954aae516ef6df7b5db7828771a1f3fcf1b6d65389ec8be8c46057a3c01fde6190001a6028d13cc988f55c8765e3ffcdcfc7d5185a8ebd68709c0adbe37b528557b01fdf20c001cc64f09a217e1971cabe751b925f246e3c2a8e145c49be7b831eaea3e064d7501fd7806009ccf122626a20cdb054877ef3f8ae2d0503bb7a8704fdb6295b3001b5e8876a101fd3d0300aeea966733175ff60b55bc77edcb83c0fce3453329f51195e5cbc7a874ee47ad01fd9f0100f67f50b53d73ffd6e84c02ee1903074b9a5b2ac64c508f7f26349b73cca9d7e901ce006ce74c7beed0c61c50dda8b578f0c0dc5a393e1f8758af2fb65edf483afcaa68016600e32475e17bdd141d62524d0005989dd1db6ca92c6af70791b0e4802be4c5c8c1013200b88162f494f26cc3a1a4a7dcf2829a295064e93b3dbb2f72e21a73522869277a011800a938d3f80dd25b6a3a80e450403bf7d62a1068e2e4b13f0656c83f764c55bb77010d006feac6e4fea41c37c508b5bfdc00d582f6e462e6754b338c95b448df37bd342c010700bf5448356be23b2b9afe53d00cee047065bbc16d0bbcc5f80aa8c1b509e45678010200c2e37431a437ee311a737aecd3caae1213db353847f33792fd539e380bdb4d440100005d5aef298770e2702448af2ce014f8bfcded5896df5006a44b5f1b6020007aeb01010091484f513003fcdb25f336b9b56dafcb05fbc739593ab573a2c6516b344ca5320201000000027b0a1b12c7c9e48015e78d3a08a4d62e439387df7e0d7a810ebd4af37661daaa000000006a47304402207d972759afba7c0ffa6cfbbf39a31c2aeede1dae28d8841db56c6dd1197d56a20220076a390948c235ba8e72b8e43a7b4d4119f1a81a77032aa6e7b7a51be5e13845412103f78ec31cf94ca8d75fb1333ad9fc884e2d489422034a1efc9d66a3b72eddca0fffffffff7f36874f858fb43ffcf4f9e3047825619bad0e92d4b9ad4ba5111d1101cbddfe010000006a473044022043f048043d56eb6f75024808b78f18808b7ab45609e4c4c319e3a27f8246fc3002204b67766b62f58bf6f30ea608eaba76b8524ed49f67a90f80ac08a9b96a6922cd41210254a583c1c51a06e10fab79ddf922915da5f5c1791ef87739f40cb68638397248ffffffff03e8030000000000001976a914b08f70bc5010fb026de018f19e7792385a146b4a88acf3010000000000001976a9147d48635f889372c3da12d75ce246c59f4ab907ed88acf7000000000000001976a914b8fbd58685b6920d8f9a8f1b274d8696708b51b088ac00000000010001000000018ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160c000000006b483045022100e47fbd96b59e2c22be273dcacea74a4be568b3e61da7eddddb6ce43d459c4cf202201a580f3d9442d5dce3f2ced03256ca147bcd230975a6067954e22415715f4490412102b0c8980f5d2cab77c92c68ac46442feba163a9d306913f6a34911fc618c3c4e7ffffffff0188130000000000001976a9148a8c4546a95e6fc8d18076a9980d59fd882b4e6988ac0000000000", + expectedErr: validation.NewError(errors.New("transaction fee of 0 sat is too low - minimum expected fee is 1 sat"), api.ErrStatusFees), + expectedErrTxID: "8184849de6af441c7de088428073c2a9131b08f7d878d9f49c3faf6d941eb168", + }, + { + name: "invlid beef - invalid scripts", + beefStr: "0100beef01fe4e6d0c001002fd909002088a382ec07a8cf47c6158b68e5822852362102d8571482d1257e0b7527e1882fd91900065cb01218f2506bb51155d243e4d6b32d69d1b5f2221c52e26963cfd8cf7283201fd4948008d7a44ae384797b0ae84db0c857e8c1083425d64d09ef8bc5e2e9d270677260501fd25240060f38aa33631c8d70adbac1213e7a5b418c90414e919e3a12ced63dd152fd85a01fd1312005ff132ee64a7a0c79150a29f66ef861e552d3a05b47d6303f5d8a2b2a09bc61501fd080900cc0baf21cf06b9439dfe05dce9bdb14ddc2ca2d560b1138296ef5769851a84b301fd85040063ccb26232a6e1d3becdb47a0f19a67a562b754e8894155b3ae7bba10335ce5101fd430200e153fc455a0f2c8372885c11af70af904dcf44740b9ebf3b3e5b2234cce550bc01fd20010077d5ea69d1dcc379dde65d6adcebde1838190118a8fae928c037275e78bd87910191000263e4f31684a25169857f2788aeef603504931f92585f02c4c9e023b2aa43d1014900de72292e0b3e5eeacfa2b657bf4d46c885559b081ee78632a99b318c1148d85c01250068a5f831ca99b9e7f3720920d6ea977fd2ab52b83d1a6567dafa4c8cafd941ed0113006a0b91d83f9056b702d6a8056af6365c7da626fc3818b815dd4b0de22d05450f0108009876ce56b68545a75859e93d200bdde7880d46f39384818b259ed847a9664ddf010500990bc5e95cacbc927b5786ec39a183f983fe160d52829cf47521c7eb369771c30103004fe794e50305f590b6010a51d050bf47dfeaabfdb949c5ee0673f577a59537d70100004dad44a358aea4d8bc1917912539901f5ae44e07a4748e1a9d3018814b0759d0020100000002704273c86298166ac351c3aa9ac90a8029e4213b5f1b03c3bbf4bc5fb09cdd43010000006a4730440220398d6389e8a156a3c6c1ca355e446d844fd480193a93af832afd1c87d0f04784022050091076b8f7405b37ce6e795d1b92526396ac2b14f08e91649b908e711e2b044121030ef6975d46dbab4b632ef62fdbe97de56d183be1acc0be641d2c400ae01cf136ffffffff2f41ed6a2488ac3ba4a3c330a15fa8193af87f0192aa59935e6c6401d92dc3a00a0000006a47304402200ad9cf0dc9c90a4c58b08910740b4a8b3e1a7e37db1bc5f656361b93f412883d0220380b6b3d587103fc8bf3fe7bed19ab375766984c67ebb7d43c993bcd199f32a441210205ef4171f58213b5a2ddf16ac6038c10a2a8c3edc1e6275cb943af4bb3a58182ffffffff03e8030000000000001976a9148a8c4546a95e6fc8d18076a9980d59fd882b4e6988acf4010000000000001976a914c7662da5e0a6a179141a7872045538126f1e954288acf5000000000000001976a914765bdf10934f5aac894cf8a3795c9eeb494c013488ac0000000001000100000001088a382ec07a8cf47c6158b68e5822852362102d8571482d1257e0b7527e1882000000006a4730440220610bba9ed83a47641c34bbbcf8eeb536d2ae6cfddc7644a8c520bb747f798c3702206a23c9f45273772dd7e80ba21a5c4613d6ffe7ba1c75b729eae0cdd484fee2bd412103c0cd91af135d09f98d57e34af28e307daf36bccd4764708e8a3f7ea5cebf01a9ffffffff01c8000000000000001976a9148ce2d21f9a75e98600be76b25b91c4fef6b40bcd88ac0000000000", + expectedErr: validation.NewError(errors.New("invalid script"), api.ErrStatusUnlockingScripts), + expectedErrTxID: "ea2924da32c47b9942cda5ad30d3c01610ca554ca3a9ca01b2ccfe72bf0667be", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + beefHex, err := hex.DecodeString(tc.beefStr) + require.NoError(t, err) + + beef, _, err := beef.DecodeBEEF(beefHex) + require.NoError(t, err) + + policy := getPolicy(1) + validator := New(policy) + + errTx, err := validator.ValidateTransaction(context.TODO(), beef, validation.StandardFeeValidation, validation.StandardScriptValidation) + assert.Equal(t, tc.expectedErr, err) + if tc.expectedErrTxID != "" { + assert.NotNil(t, errTx) + assert.Equal(t, tc.expectedErrTxID, errTx.TxID()) + } + }) + } +} + func TestCalculateInputOutputsSatoshis(t *testing.T) { testCases := []struct { name string @@ -41,10 +104,10 @@ func TestCalculateInputOutputsSatoshis(t *testing.T) { beefHex, err := hex.DecodeString(tc.beefStr) require.NoError(t, err) - beefTx, _, err := DecodeBEEF(beefHex) + beefTx, _, err := beef.DecodeBEEF(beefHex) require.NoError(t, err) - inputs, outputs, err := CalculateInputsOutputsSatoshis(beefTx.GetLatestTx(), beefTx.Transactions) + inputs, outputs, err := calculateInputsOutputsSatoshis(beefTx.GetLatestTx(), beefTx.Transactions) assert.NoError(t, err) assert.Equal(t, tc.expectedInputs, inputs) assert.Equal(t, tc.expectedOutputs, outputs) @@ -71,7 +134,7 @@ func TestValidateScripts(t *testing.T) { { name: "Invlid Beef - Invalid Scripts", beefStr: "0100beef01fe4e6d0c001002fd909002088a382ec07a8cf47c6158b68e5822852362102d8571482d1257e0b7527e1882fd91900065cb01218f2506bb51155d243e4d6b32d69d1b5f2221c52e26963cfd8cf7283201fd4948008d7a44ae384797b0ae84db0c857e8c1083425d64d09ef8bc5e2e9d270677260501fd25240060f38aa33631c8d70adbac1213e7a5b418c90414e919e3a12ced63dd152fd85a01fd1312005ff132ee64a7a0c79150a29f66ef861e552d3a05b47d6303f5d8a2b2a09bc61501fd080900cc0baf21cf06b9439dfe05dce9bdb14ddc2ca2d560b1138296ef5769851a84b301fd85040063ccb26232a6e1d3becdb47a0f19a67a562b754e8894155b3ae7bba10335ce5101fd430200e153fc455a0f2c8372885c11af70af904dcf44740b9ebf3b3e5b2234cce550bc01fd20010077d5ea69d1dcc379dde65d6adcebde1838190118a8fae928c037275e78bd87910191000263e4f31684a25169857f2788aeef603504931f92585f02c4c9e023b2aa43d1014900de72292e0b3e5eeacfa2b657bf4d46c885559b081ee78632a99b318c1148d85c01250068a5f831ca99b9e7f3720920d6ea977fd2ab52b83d1a6567dafa4c8cafd941ed0113006a0b91d83f9056b702d6a8056af6365c7da626fc3818b815dd4b0de22d05450f0108009876ce56b68545a75859e93d200bdde7880d46f39384818b259ed847a9664ddf010500990bc5e95cacbc927b5786ec39a183f983fe160d52829cf47521c7eb369771c30103004fe794e50305f590b6010a51d050bf47dfeaabfdb949c5ee0673f577a59537d70100004dad44a358aea4d8bc1917912539901f5ae44e07a4748e1a9d3018814b0759d0020100000002704273c86298166ac351c3aa9ac90a8029e4213b5f1b03c3bbf4bc5fb09cdd43010000006a4730440220398d6389e8a156a3c6c1ca355e446d844fd480193a93af832afd1c87d0f04784022050091076b8f7405b37ce6e795d1b92526396ac2b14f08e91649b908e711e2b044121030ef6975d46dbab4b632ef62fdbe97de56d183be1acc0be641d2c400ae01cf136ffffffff2f41ed6a2488ac3ba4a3c330a15fa8193af87f0192aa59935e6c6401d92dc3a00a0000006a47304402200ad9cf0dc9c90a4c58b08910740b4a8b3e1a7e37db1bc5f656361b93f412883d0220380b6b3d587103fc8bf3fe7bed19ab375766984c67ebb7d43c993bcd199f32a441210205ef4171f58213b5a2ddf16ac6038c10a2a8c3edc1e6275cb943af4bb3a58182ffffffff03e8030000000000001976a9148a8c4546a95e6fc8d18076a9980d59fd882b4e6988acf4010000000000001976a914c7662da5e0a6a179141a7872045538126f1e954288acf5000000000000001976a914765bdf10934f5aac894cf8a3795c9eeb494c013488ac0000000001000100000001088a382ec07a8cf47c6158b68e5822852362102d8571482d1257e0b7527e1882000000006a4730440220610bba9ed83a47641c34bbbcf8eeb536d2ae6cfddc7644a8c520bb747f798c3702206a23c9f45273772dd7e80ba21a5c4613d6ffe7ba1c75b729eae0cdd484fee2bd412103c0cd91af135d09f98d57e34af28e307daf36bccd4764708e8a3f7ea5cebf01a9ffffffff01c8000000000000001976a9148ce2d21f9a75e98600be76b25b91c4fef6b40bcd88ac0000000000", - expectedError: errors.New("invalid script"), + expectedError: validation.NewError(errors.New("invalid script"), api.ErrStatusUnlockingScripts), }, } @@ -80,10 +143,10 @@ func TestValidateScripts(t *testing.T) { beefHex, err := hex.DecodeString(tc.beefStr) require.NoError(t, err) - beefTx, _, err := DecodeBEEF(beefHex) + beefTx, _, err := beef.DecodeBEEF(beefHex) require.NoError(t, err) - err = ValidateScripts(beefTx.GetLatestTx(), beefTx.Transactions) + err = validateScripts(beefTx.GetLatestTx(), beefTx.Transactions) assert.Equal(t, tc.expectedError, err) }) } @@ -121,10 +184,19 @@ func TestEnsureAncestorsArePresentInBump(t *testing.T) { beefHex, err := hex.DecodeString(tc.beefStr) require.NoError(t, err) - beefTx, _, err := DecodeBEEF(beefHex) + beefTx, _, err := beef.DecodeBEEF(beefHex) require.NoError(t, err) - err = EnsureAncestorsArePresentInBump(beefTx.GetLatestTx(), beefTx) + err = ensureAncestorsArePresentInBump(beefTx.GetLatestTx(), beefTx) assert.Equal(t, tc.expectedError, err) } } + +func getPolicy(satoshisPerKB uint64) *bitcoin.Settings { + var policy *bitcoin.Settings + + _ = json.Unmarshal([]byte(testdata.DefaultPolicy), &policy) + + policy.MinMiningTxFee = float64(satoshisPerKB) / 1e8 + return policy +} diff --git a/internal/validator/default/default_validator.go b/internal/validator/default/default_validator.go index 3fe2c05d8..804166bc6 100644 --- a/internal/validator/default/default_validator.go +++ b/internal/validator/default/default_validator.go @@ -1,39 +1,27 @@ package defaultvalidator import ( - "encoding/hex" + "context" "fmt" - "math" - "github.com/bitcoin-sv/arc/internal/beef" "github.com/bitcoin-sv/arc/internal/validator" "github.com/bitcoin-sv/arc/pkg/api" "github.com/libsv/go-bt/v2" - "github.com/libsv/go-bt/v2/bscript" "github.com/libsv/go-bt/v2/bscript/interpreter" "github.com/ordishs/go-bitcoin" ) -// MaxBlockSize is set dynamically in a node, and should be gotten from the policy -const ( - MaxBlockSize = 4 * 1024 * 1024 * 1024 - MaxSatoshis = 21_000_000_00_000_000 - coinbaseTxID = "0000000000000000000000000000000000000000000000000000000000000000" - MaxTxSigopsCountPolicyAfterGenesis = ^uint32(0) // UINT32_MAX - minTxSizeBytes = 61 -) - type DefaultValidator struct { policy *bitcoin.Settings } -func New(policy *bitcoin.Settings) validator.Validator { +func New(policy *bitcoin.Settings) *DefaultValidator { return &DefaultValidator{ policy: policy, } } -func (v *DefaultValidator) ValidateEFTransaction(tx *bt.Tx, skipFeeValidation bool, skipScriptValidation bool) error { //nolint:funlen - mostly comments +func (v *DefaultValidator) ValidateTransaction(ctx context.Context, tx *bt.Tx, feeValidation validator.FeeValidation, scriptValidation validator.ScriptValidation) error { //nolint:funlen - mostly comments // 0) Check whether we have a complete transaction in extended format, with all input information // we cannot check the satoshi input, OP_RETURN is allowed 0 satoshis if !v.IsExtended(tx) { @@ -41,107 +29,25 @@ func (v *DefaultValidator) ValidateEFTransaction(tx *bt.Tx, skipFeeValidation bo } // The rest of the validation steps - err := v.validateTransaction(tx, skipFeeValidation, skipScriptValidation) + err := validator.CommonValidateTransaction(v.policy, tx) if err != nil { return err } - // everything checks out - return nil -} - -func (v *DefaultValidator) ValidateBeef(beefTx *beef.BEEF, skipFeeValidation, skipScriptValidation bool) (*bt.Tx, error) { - for _, btx := range beefTx.Transactions { - // verify only unmined transactions - if btx.IsMined() { - continue - } - - tx := btx.Transaction - - // needs to be calculated here, because txs in beef are not in EF format - if !skipFeeValidation { - if err := v.validateBeefFees(tx, beefTx); err != nil { - return tx, err - } - } - - // needs to be calculated here, because txs in beef are not in EF format - if !skipScriptValidation { - if err := beef.ValidateScripts(tx, beefTx.Transactions); err != nil { - return tx, validator.NewError(err, api.ErrStatusUnlockingScripts) - } - } - - // purposefully skip the fee and scripts validation, because it's done above - if err := v.validateTransaction(tx, true, true); err != nil { - return tx, err - } - } - - if err := beef.EnsureAncestorsArePresentInBump(beefTx.GetLatestTx(), beefTx); err != nil { - return beefTx.GetLatestTx(), validator.NewError(err, api.ErrStatusMinedAncestorsNotFound) - } - - return nil, nil -} - -func (v *DefaultValidator) validateTransaction(tx *bt.Tx, skipFeeValidation, skipScriptValidation bool) error { - // - // Each node will verify every transaction against a long checklist of criteria: - // - txSize := tx.Size() - - // 1) Neither lists of inputs or outputs are empty - if len(tx.Inputs) == 0 || len(tx.Outputs) == 0 { - return validator.NewError(fmt.Errorf("transaction has no inputs or outputs"), api.ErrStatusInputs) - } - - // 2) The transaction size in bytes is less than maxtxsizepolicy. - if err := checkTxSize(txSize, v.policy); err != nil { - return validator.NewError(err, api.ErrStatusTxFormat) - } - - // 3) check that each input value, as well as the sum, are in the allowed range of values (less than 21m coins) - // 5) None of the inputs have hash=0, N=–1 (coinbase transactions should not be relayed) - if err := checkInputs(tx); err != nil { - return validator.NewError(err, api.ErrStatusInputs) - } - - // 4) Each output value, as well as the total, must be within the allowed range of values (less than 21m coins, - // more than the dust threshold if 1 unless it's OP_RETURN, which is allowed to be 0) - if err := checkOutputs(tx); err != nil { - return validator.NewError(err, api.ErrStatusOutputs) - } - - // 6) nLocktime is equal to INT_MAX, or nLocktime and nSequence values are satisfied according to MedianTimePast - // => checked by the node, we do not want to have to know the current block height - - // 7) The transaction size in bytes is greater than or equal to 100 - if txSize < minTxSizeBytes { - return validator.NewError(fmt.Errorf("transaction size in bytes is less than %d bytes", minTxSizeBytes), api.ErrStatusMalformed) - } - - // 8) The number of signature operations (SIGOPS) contained in the transaction is less than the signature operation limit - if err := sigOpsCheck(tx, v.policy); err != nil { - return validator.NewError(err, api.ErrStatusMalformed) - } - - // 9) The unlocking script (scriptSig) can only push numbers on the stack - if err := pushDataCheck(tx); err != nil { - return validator.NewError(err, api.ErrStatusMalformed) - } - // 10) Reject if the sum of input values is less than sum of output values // 11) Reject if transaction fee would be too low (minRelayTxFee) to get into an empty block. - if !skipFeeValidation { - if err := checkFees(tx, api.FeesToBtFeeQuote(v.policy.MinMiningTxFee)); err != nil { - return validator.NewError(err, api.ErrStatusFees) + switch feeValidation { + case validator.StandardFeeValidation: + if err := standardCheckFees(tx, api.FeesToBtFeeQuote(v.policy.MinMiningTxFee)); err != nil { + return err } + case validator.NoneFeeValidation: + fallthrough + default: } // 12) The unlocking scripts for each input must validate against the corresponding output locking scripts - if !skipScriptValidation { + if scriptValidation == validator.StandardScriptValidation { if err := checkScripts(tx); err != nil { return validator.NewError(err, api.ErrStatusUnlockingScripts) } @@ -151,32 +57,6 @@ func (v *DefaultValidator) validateTransaction(tx *bt.Tx, skipFeeValidation, ski return nil } -func (v *DefaultValidator) validateBeefFees(tx *bt.Tx, beefTx *beef.BEEF) error { - expectedFees, err := calculateMiningFeesRequired(tx.SizeWithTypes(), api.FeesToBtFeeQuote(v.policy.MinMiningTxFee)) - if err != nil { - return validator.NewError(err, api.ErrStatusFees) - } - - inputSatoshis, outputSatoshis, err := beef.CalculateInputsOutputsSatoshis(tx, beefTx.Transactions) - if err != nil { - return validator.NewError(err, api.ErrStatusFees) - } - - actualFeePaid := inputSatoshis - outputSatoshis - - if inputSatoshis < outputSatoshis { - // force an error without wrong negative values - actualFeePaid = 0 - } - - if actualFeePaid < expectedFees { - err := fmt.Errorf("transaction fee of %d sat is too low - minimum expected fee is %d sat", actualFeePaid, expectedFees) - return validator.NewError(err, api.ErrStatusFees) - } - - return nil -} - func (v *DefaultValidator) IsExtended(tx *bt.Tx) bool { if tx == nil || tx.Inputs == nil { return false @@ -191,82 +71,22 @@ func (v *DefaultValidator) IsExtended(tx *bt.Tx) bool { return true } -func (v *DefaultValidator) IsBeef(txHex []byte) bool { - return beef.CheckBeefFormat(txHex) -} - -func checkTxSize(txSize int, policy *bitcoin.Settings) error { - maxTxSizePolicy := policy.MaxTxSizePolicy - if maxTxSizePolicy == 0 { - // no policy found for tx size, use max block size - maxTxSizePolicy = MaxBlockSize - } - if txSize > maxTxSizePolicy { - return fmt.Errorf("transaction size in bytes is greater than max tx size policy %d", maxTxSizePolicy) - } - - return nil -} - -func checkOutputs(tx *bt.Tx) error { - total := uint64(0) - for index, output := range tx.Outputs { - isData := output.LockingScript.IsData() - switch { - case !isData && (output.Satoshis > MaxSatoshis || output.Satoshis < bt.DustLimit): - return validator.NewError(fmt.Errorf("transaction output %d satoshis is invalid", index), api.ErrStatusOutputs) - case isData && output.Satoshis != 0: - return validator.NewError(fmt.Errorf("transaction output %d has non 0 value op return", index), api.ErrStatusOutputs) - } - total += output.Satoshis - } - - if total > MaxSatoshis { - return validator.NewError(fmt.Errorf("transaction output total satoshis is too high"), api.ErrStatusOutputs) - } - - return nil -} - -func checkInputs(tx *bt.Tx) error { - total := uint64(0) - for index, input := range tx.Inputs { - if hex.EncodeToString(input.PreviousTxID()) == coinbaseTxID { - return validator.NewError(fmt.Errorf("transaction input %d is a coinbase input", index), api.ErrStatusInputs) - } - /* lots of our valid test transactions have this sequence number, is this not allowed? - if input.SequenceNumber == 0xffffffff { - fmt.Printf("input %d has sequence number 0xffffffff, txid = %s", index, tx.TxID()) - return validator.NewError(fmt.Errorf("transaction input %d sequence number is invalid", index), arc.ErrStatusInputs) - } - */ - if input.PreviousTxSatoshis > MaxSatoshis { - return validator.NewError(fmt.Errorf("transaction input %d satoshis is too high", index), api.ErrStatusInputs) - } - total += input.PreviousTxSatoshis - } - if total > MaxSatoshis { - return validator.NewError(fmt.Errorf("transaction input total satoshis is too high"), api.ErrStatusInputs) - } - - return nil -} - -func checkFees(tx *bt.Tx, feeQuote *bt.FeeQuote) error { +func standardCheckFees(tx *bt.Tx, feeQuote *bt.FeeQuote) error { feesOK, expFeesPaid, actualFeePaid, err := isFeePaidEnough(feeQuote, tx) if err != nil { - return err + return validator.NewError(err, api.ErrStatusFees) } if !feesOK { - return fmt.Errorf("transaction fee of %d sat is too low - minimum expected fee is %d sat", actualFeePaid, expFeesPaid) + err = fmt.Errorf("transaction fee of %d sat is too low - minimum expected fee is %d sat", actualFeePaid, expFeesPaid) + return validator.NewError(err, api.ErrStatusFees) } return nil } func isFeePaidEnough(fees *bt.FeeQuote, tx *bt.Tx) (bool, uint64, uint64, error) { - expFeesPaid, err := calculateMiningFeesRequired(tx.SizeWithTypes(), fees) + expFeesPaid, err := validator.CalculateMiningFeesRequired(tx.SizeWithTypes(), fees) if err != nil { return false, 0, 0, err } @@ -282,93 +102,6 @@ func isFeePaidEnough(fees *bt.FeeQuote, tx *bt.Tx) (bool, uint64, uint64, error) return actualFeePaid >= expFeesPaid, expFeesPaid, actualFeePaid, nil } -func calculateMiningFeesRequired(size *bt.TxSize, fees *bt.FeeQuote) (uint64, error) { - var feesRequired float64 - - feeStandard, err := fees.Fee(bt.FeeTypeStandard) - if err != nil { - return 0, err - } - - feesRequired += float64(size.TotalStdBytes) * float64(feeStandard.MiningFee.Satoshis) / float64(feeStandard.MiningFee.Bytes) - - feeData, err := fees.Fee(bt.FeeTypeData) - if err != nil { - return 0, err - } - - feesRequired += float64(size.TotalDataBytes) * float64(feeData.MiningFee.Satoshis) / float64(feeData.MiningFee.Bytes) - - // the minimum fees required is 1 satoshi - feesRequiredRounded := uint64(math.Round(feesRequired)) - if feesRequiredRounded < 1 { - feesRequiredRounded = 1 - } - - return feesRequiredRounded, nil -} - -func sigOpsCheck(tx *bt.Tx, policy *bitcoin.Settings) error { - maxSigOps := policy.MaxTxSigopsCountsPolicy - - if maxSigOps == 0 { - maxSigOps = int64(MaxTxSigopsCountPolicyAfterGenesis) - } - - parser := interpreter.DefaultOpcodeParser{} - numSigOps := int64(0) - - for _, input := range tx.Inputs { - parsedUnlockingScript, err := parser.Parse(input.UnlockingScript) - if err != nil { - return err - } - - for _, op := range parsedUnlockingScript { - if op.Value() == bscript.OpCHECKSIG || op.Value() == bscript.OpCHECKSIGVERIFY { - numSigOps++ - } - } - } - - for _, output := range tx.Outputs { - parsedLockingScript, err := parser.Parse(output.LockingScript) - if err != nil { - return err - } - - for _, op := range parsedLockingScript { - if op.Value() == bscript.OpCHECKSIG || op.Value() == bscript.OpCHECKSIGVERIFY { - numSigOps++ - } - } - } - - if numSigOps > maxSigOps { - return fmt.Errorf("transaction unlocking scripts have too many sigops (%d)", numSigOps) - } - - return nil -} - -func pushDataCheck(tx *bt.Tx) error { - for index, input := range tx.Inputs { - if input.UnlockingScript == nil { - return fmt.Errorf("transaction input %d unlocking script is empty", index) - } - parser := interpreter.DefaultOpcodeParser{} - parsedUnlockingScript, err := parser.Parse(input.UnlockingScript) - if err != nil { - return err - } - if !parsedUnlockingScript.IsPushOnly() { - return fmt.Errorf("transaction input %d unlocking script is not push only", index) - } - } - - return nil -} - func checkScripts(tx *bt.Tx) error { for i, in := range tx.Inputs { prevOutput := &bt.Output{ @@ -380,7 +113,6 @@ func checkScripts(tx *bt.Tx) error { interpreter.WithTx(tx, i, prevOutput), interpreter.WithForkID(), interpreter.WithAfterGenesis(), - // interpreter.WithDebugger(&LogDebugger{}), ); err != nil { return fmt.Errorf("script execution failed: %w", err) } diff --git a/internal/validator/default/default_validator_test.go b/internal/validator/default/default_validator_test.go index 50ff84028..d07d86d5c 100644 --- a/internal/validator/default/default_validator_test.go +++ b/internal/validator/default/default_validator_test.go @@ -1,21 +1,17 @@ package defaultvalidator import ( - "encoding/hex" + "context" "encoding/json" - "errors" "os" "testing" - "github.com/bitcoin-sv/arc/internal/beef" "github.com/bitcoin-sv/arc/internal/testdata" - "github.com/bitcoin-sv/arc/internal/validator" - "github.com/bitcoin-sv/arc/pkg/api" + validation "github.com/bitcoin-sv/arc/internal/validator" "github.com/libsv/go-bt/v2" "github.com/libsv/go-bt/v2/bscript" "github.com/ordishs/go-bitcoin" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,10 +19,6 @@ var validLockingScript = &bscript.Script{ 0x76, 0xa9, 0x14, 0xcd, 0x43, 0xba, 0x65, 0xce, 0x83, 0x77, 0x8e, 0xf0, 0x4b, 0x20, 0x7d, 0xe1, 0x44, 0x98, 0x44, 0x0f, 0x3b, 0xd4, 0x6c, 0x88, 0xac, } -var opReturnLockingScript = &bscript.Script{ - 0x00, 0x6a, 0x4c, 0x4d, 0x41, 0x50, 0x49, 0x20, 0x30, 0x2e, 0x31, 0x2e, 0x30, 0x20, 0x2d, 0x20, -} - const ( opReturnTx = "010000000000000000ef01478a4ac0c8e4dae42db983bc720d95ed2099dec4c8c3f2d9eedfbeb74e18cdbb1b0100006b483045022100b05368f9855a28f21d3cb6f3e278752d3c5202f1de927862bbaaf5ef7d67adc50220728d4671cd4c34b1fa28d15d5cd2712b68166ea885522baa35c0b9e399fe9ed74121030d4ad284751daf629af387b1af30e02cf5794139c4e05836b43b1ca376624f7fffffffff10000000000000001976a9140c77a935b45abdcf3e472606d3bc647c5cc0efee88ac01000000000000000070006a0963657274696861736822314c6d763150594d70387339594a556e374d3948565473446b64626155386b514e4a406164386337373536356335363935353261626463636634646362353537376164633936633866613933623332663630373865353664666232326265623766353600000000" runTx = "010000000000000000ef0288e59c195e017a9606fcaa21ae75ae670b8d1042380db5eb1860dff6868d349d010000006a4730440220771f717cab9acf745b2448b057b720913c503989262a5291edfd00a7a151fa5e02200d5c5cdd0b9320a796ba7c4e196ff04d5d7be8e7ca069c9af59bb8a2da5dfb41412102028571938947eeceeefac38f0a59f460ea57dc2922047240c1a777cb02261936ffffffff11010000000000001976a91428566dfea52b366fa3f545f7e4ab4392d48ddaae88ac19cb57677947f90549a8b7a207563fe254edce80c042e3ddf06e84e78e6e0934010000006a473044022036bffed646b47f6becea192696b3bf4c4bbee80c29cbc79a9e598c6dce895d3502205e5bc389e805d05b23684469666d8cc81ad3635445df6e8a344d27962016ce94412102213568f72dc2aa813f0154b80d5492157e5c47e69ce0d0ec421d8e3fdb1cde6affffffff404b4c00000000001976a91428c115c42ec654230f1666637d2e72808b1ff46d88ac030000000000000000b1006a0372756e0105004ca67b22696e223a312c22726566223a5b5d2c226f7574223a5b5d2c2264656c223a5b2231376238623534616237363066306635363230393561316664336432306533353865623530653366383638626535393230346462386333343939363337323135225d2c22637265223a5b5d2c2265786563223a5b7b226f70223a2243414c4c222c2264617461223a5b7b22246a6967223a307d2c2264657374726f79222c5b5d5d7d5d7d404b4c00000000001976a91488c05fb97867cab4f4875e5cd4c96929c15f1ca988acf4000000000000001976a9149f4fa07a87b9169f2a66a0456c0c8d4f1209504f88ac00000000" @@ -42,7 +34,7 @@ func TestValidator(t *testing.T) { policy := getPolicy(500) validator := New(policy) - err := validator.ValidateEFTransaction(tx, false, false) + err := validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) require.NoError(t, err) }) @@ -53,7 +45,7 @@ func TestValidator(t *testing.T) { policy := getPolicy(500) validator := New(policy) - err := validator.ValidateEFTransaction(tx, false, false) + err := validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) require.Error(t, err, "Validation should have returned an error") if err != nil { @@ -70,7 +62,7 @@ func TestValidator(t *testing.T) { policy := getPolicy(500) validator := New(policy) - err := validator.ValidateEFTransaction(tx, false, false) + err := validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) require.Error(t, err) }) @@ -80,7 +72,7 @@ func TestValidator(t *testing.T) { policy := getPolicy(5) validator := New(policy) - err := validator.ValidateEFTransaction(tx, false, false) + err := validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) require.NoError(t, err) }) @@ -98,7 +90,7 @@ func TestValidator(t *testing.T) { policy := getPolicy(5) validator := New(policy) - err = validator.ValidateEFTransaction(tx, false, false) + err = validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) require.NoError(t, err, "Failed to validate tx %d", txIndex) } }) @@ -128,67 +120,11 @@ func TestValidator(t *testing.T) { policy := getPolicy(5) validator := New(policy) - err = validator.ValidateEFTransaction(tx, false, false) + err = validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) require.NoError(t, err, "Failed to validate tx") }) } -func TestBeefValidator(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - beefStr string - expectedErr error - expectedErrTxID string - }{ - { - name: "valid fully mined beef", - beefStr: "0100beef04fe4c7c0c000e02fd460302d2e6252f996ab1a6fcbe8911e8f865bb719c0e11787397fd818b5bb1ff554a3cfd470300d67d0a32df0ecd5976479f068f19ff671e0a285570b915bc6e6658e98a9c0e3401fda20100eafe36c6f1adef584b1d199286c5024a4791eb16f35161c4b69cc119c1c9493c01d00077605d020124b1f6e039f1b2ccebf3e46acf45584bc559b654b57179b99d906a0169002c4fe7d1d2b2990df28e5132b059e10cc6ce9be85a79f834546eee881a8f968d013500a187befab2d1cc22004f96045afecd929e2edc7017a044aaf0f530847afa1644011b00bf5108b8176d4abba8c80f935607abc6a6bc38b9461c86a812369975ad2abb1e010c002efe8c4f9a630b0d0f4a410d93523d7003085dd166248d6e76f405fe8fc606720107007043c5b1be3e6952859bca6d6320f5a0f6c5bba5d5fbfd8a195f1d9ec4e0964c010200822378b29fd273a3bf38a4b852089e8f5a1251b5fedeee6ee2f548c0ee93a14e010000788ff2bf42a41c2e32bd59504fa59fd8b2ab0058430f9d4d2988ee8341ccac1b010100470c98a5fb48ed000ca746a05c7d398d2b76efe70a65c67c1cec14a8c8c84cce010100de821cfed42c83ab4dfc3fef452ec5f4086d18fb7324c046826cd51e0fea2c8b0101006422ad7eb4999dc4ca89a3488862f03b4101c32ed49c19e3df7563958e6e480201010087ce3715a94573ac7746f3b66e4070d313b4a8df6114509678ab08c425250b06fe137c0c000e04fde00a02827f1758c64b4a0b1226c54316ddeed4618500f026602ebca3cd4b96174a690afde10a001a906867a2cbd98095453400107f814e5380640443bf4504eb6fe412acba674bfdac0d001f3da43722e72c749846913dbce0623b1d22fa8499bbca49f3f9aec4cd5b3d70fdad0d02213e4fca3103f812ffcba253caf452c6811947ff6f2fb99b4e18baa1233e84b602fd7105001399fab1150a4f8013dca738bec4ae8c5b215e301697d6453f648230b36cd431fdd70600282c33fe4ec70f91818781daa6a97e85022bc2031a370202a385f2aeb9086a1a02fdb90200fd35a389307434d05891122dc81f3a5246f577334de368ebcfc15836648e73a8fd6a0300ed1732da6c0666a7270d63ca0d4af8ba1c9a80c149bccd5e1ac039159418740a02fd5d010019dc6865859cc67245c5d9b04bc3a9141a4e6fda17ba33d24271600244b2dd35fdb40100ccb07e295f3bbac75957015c58c5f7f64be4904865c95ebf4181c40d0089429602af002d31340de572c9cdbe9c75c62474ad3130e417d7a61b370aab7345901f69badfdb006c6b6e5958eb5f2ae497e4b7d8ecc90ac614856ada2458f35e1f2880fd155530025600efb95e533fbed11730d7eb9f2c891a885cb658f4ca058fdf6bed8d348c9415de6c0056e17aded7bd785e6a44cf023a294440bd89886dfa5d48ce28386199361e4b47022a003c5a68cac12c161d6a3c5a1e8ba77d9df937954a3c5ebb669fc5d3f04883c22237003978958e578f7326e8271b058ad60c85f1d2d404bdf75611ef9192de27ce644d021400692e79f2246540d6526e8261bda1b430f218be172a2672ae0a58e5fe20c359331a00595cbb11c5591f7630c31a779fe71b5a48526b525078a37492b087dcff4f1314020b00d6a30d5bccc4f3d9387e56e9e1f32de9db91c52c8fb4179aab7732a38df8df600c009fe2e3698677771d5fd4819796ee8342277ae3bdcd68e4e38ad3c04d207df06d0204007635530acf3aff406ca2ae825aa7253c3c1fb92cadf0124145a4565477c9198f0700f1f84a4c5efaeb4d5660968aa9234db2a98c5bca3f5942ec1d007dc2d996822f0202003578890ece8c5028080f613c01b8662df60c4c5ea6cc858e35ec266f3e8c8bf80300d2fcdb70b5967e0b53341b6bcc517511aa977ec6c98e587035e734bafb2df40201000050e8890913d743a6a49e244dab275bcafb7ec2fd8828163e6a07560351b77b2e01010039787d0d5252e1347883ebcad07c141448e366d2b40d3241211482ef52f34d97010100278ecd209c80ea49e6581c8b9e03b118e66cbc39e4acb14b26bda7d378fe2117feeb7b0c000f02fdd438020dacf934645c462a155ca35453ab578e7d510687fda689a565e000fc4df11cd5fdd53800de97bf2bdde019a90f6c784c3731b71a45ade39a58c436e2c70a4c9f83371e1d01fd6b1c00aaccb26fe28312165b6a2fd6414c651dd178a8053324d25008b8f533af7ad30a01fd340e0010cd00845ba8b3e5e08a057de308885b01ec1cb2f4cc56da6583ff1f3f9d98ff01fd1b0700d712139ba223278e95809bd1aebd6d334288ef66b989116a344485c603e9998d01fd8c0300097e0e68b5f5aab8ef48a8983b4b4fa992d80bd4cc2fd2c3069be71407d73b0001fdc701008be1cc85f6bb068b86ad60cfbd321878c2774c99274e953530cf4d7611227f3b01e2000453717a6dee5b33d9c379e7b262d17ddcd6267b159fd8a0b980603d03ce31b601700050992dd58d2332d87df4874852f9275eeb9c7813e1a6c1e1cad1a74a9e5560a10139003fdc83bc63d0af296cd6a71e45355eab21634d829f6b17e769fe0d2308a99484011d001eb98912db04c69f675a4cfb64a57167d9b136d174c0d0e1f2635840a27a2819010f00b0a43e31d356c1ccd7cd1733f8bf5885013361d7f02627629136b360c55aa43a010600a7b2280eb6bc6f398f7f5423033bd011e819caf0beeb248f26a1498fbc7e4bb1010200152e881046cdb491d33c93d799f28e07686708db2cdc8a3011c375f7b9bef87a010000851add544d85300e318ec60be010f38f2bea85cc86ce5a972df28b94e53f5801010100a7308e947ed378dcf6e6f79ba0bb8d062f7c7ffcc025e5d08fc30a3288e87ef5fe1c7c0c000e02fd5a1902e5232220aee5069017d31cc30818bcc971de3e6418f6e62b8cc9a3430d64f3e9fd5b19004359d0c4ca1b47399960cb57ff4b0d2c850c825ba56a2119c3ff875fece11a4001fdac0c003c0fd4b7a8621c97bf7b6adad9569fe1f07a52c41d472d1e3407f9e37b3400d701fd5706006400dbcd6c1ea8a5c8cccfa153eb02076bf03b4ac7fef38db8e3a5032d908f3101fd2a0300ceac7150203519610ec78f93d64bbdc2e76e9cfadb72a83070d5fc72c8c0291a01fd94010058dde4e9d746faed13b29bd0f44ea86cdab712bc3d430a88e8cec22364e19cfd01cb001f77543e1a81dc8d4fdef4a4b132cf907b8a22de393400035a40391904fcb5c20164002cc86783ce17e19b7a56e0d08ba4c5d94cfd586771dd28e2fe1284d887db1285013300ceb2afce84f3e257bce737ebaef9ded331348d750d17f408c22d79dc8e9de9e9011800d939fb970c2044768981bea25d19be80302773a4b0d469566315258d39db5059010d00c815d2c629a1d419d6ef3c33a670da1bc64f7f23a840365fc292aab7d7b4f770010700828ced5731867ba622104fe64bd18a2132097623f8fe4a834537b329dbb6d06a010200f9df4e876faa4ddd9f909bfafed085ad24297f80003a3700bd13250498e252190100004834f3f8c1be8c4f43bb61dd6d0eea6ab996bae149aefd5f9aced05c54b3395901010036679c2ca45cd1d37b4b0d1d4a9a0e923d80f6241957fd61969b641ca1a2b62c060100000001aa5df6cc8e5f41e8770d222d06d2dfdea647769871044df714c99b59d3528858000000006b483045022100d65cb97677ba806e752d63ea988faf5fb00cabd0df78bb78fe9f42a079e7ae540220158a2d51ca8b03f18691eab1c4fa5e85b6f21846df9bfaf797c995b1c9f34e47412103597c97b42c5f880e27eaf2eb2de0fb0fe5a81d689c646b2c6527acbc9c2509c7ffffffff0264000000000000001976a914e5c8e12ec010e7bf318ebc94efcb887ff751069c88ac8e010000000000001976a914ae791b7e9d0108b4000ac4138c2b47d8cfea404288ac0000000001010100000001a25525c35101ad7ad7653e16d5d525762f3826e4c7b87077a096aebabe292ad9000000006b4830450221009fa60fe23a004850fd671a834534b278d4a2a9a4e7d8dfc0e4f9ffcb2899c1ef022010a8e718b608dfd308cdbc382e6f666583c8148b5f70819fbb7df1f1edd8cc164121023d72bbb613c7e8a4230c080ffaff1abc038df9008046b2157f888f36f427d2f0ffffffff0228000000000000001976a914596ce4dbbefd792d782ccb7ce5652726fd0c676288acca010000000000001976a9143e8a2536d8bc8f2a995800f16e068ee85aa1939288ac00000000010301000000017a7123cf30268df225315c47b6ccbfd3e4f5c19f765bb9abc8fbeed134eb0b7f010000006a47304402201715f13c3456310f7b73386ca947621b53f71f27d2a23c0d1f09deb4b8931ca7022000d9c8daa400b1c2ad34df4c22e024e0d9191ecefe83160f47fbf53026fb4d53412102b6f13a7f2599f7312cf55ca1524bf9ebefeed22daa1f80ffed82df038db0167affffffff012d000000000000001976a9148e4221cdcf3f547ffd0aee46411b40a3248fdb3688ac0000000001020100000001a25525c35101ad7ad7653e16d5d525762f3826e4c7b87077a096aebabe292ad9010000006a47304402202ed74a7c67dd15fa9594ae600775e3a289511ceda7994b7914bd94435a269c7202205767d4deaf8db2c7c3a862a2b0e8f43f377a597bbbbed10409819bc2239064804121024ffdb4c74d0f3e92dbfe41348ee5295a8a8aa91f7eea1c17c879baf3d31492c3ffffffff01e7030000000000001976a914424cc7a221a4bf81790a850014a5e1a1663d3b5888ac0000000001010100000001213e4fca3103f812ffcba253caf452c6811947ff6f2fb99b4e18baa1233e84b6000000006a47304402202b577840e0fbab66a150a79feeb9ea2e0469df83f44a4aebb4779c35ea1d7b5402204764d20a211b112e8907a3a80e418dcc933ba4e49dea7072bd5814cca8a76e664121027db94b451dafb7368da30ed73abbf064a622ebcffc1bd3362802fd8eaaf5fc6dffffffff0163000000000000001976a9144451354c29626cbeb4f130e6bb7ddd54151c6b3b88ac0000000001000100000005d2e6252f996ab1a6fcbe8911e8f865bb719c0e11787397fd818b5bb1ff554a3c000000006a4730440220179e09c88dc5d83e6db5abcf2a46c989ba766612f3be664f66f9d5932681b42e02207ea73be5317362c511e76a40f3b416ca2ec99ce891bd55fbbfc60f3e2598c1be41210277e25e8e4ab96e46d94a037411d195b537810b1d7301c2d72e9eb40bb47aae34ffffffff827f1758c64b4a0b1226c54316ddeed4618500f026602ebca3cd4b96174a690a000000006b4830450221008a9418e0be1b65b45ac27449071bf8a7cb55d9008ae5c8d28da47bce9bc2cc28022056aa8ffd170eb4b86b2be7da81b1d01dd0f026d0488645c3b27f6e521eebb51a412103138a3aac623a5fc9789cd9476efad0605dea61f3e7cd5089195eb0b446b6ab4effffffff0dacf934645c462a155ca35453ab578e7d510687fda689a565e000fc4df11cd5000000006a4730440220147729079aef1449d0c1b08f79c1fa4749d685190d0fdd25801725845d0186740220796be61c7a059e65b4e418d1fec99a596ea565b179683d334dc7a2e2312ed466412103ba49c495254796f5ccf0e28f205f62965fafc33367b2b8d6609e5de30c206ad4ffffffff213e4fca3103f812ffcba253caf452c6811947ff6f2fb99b4e18baa1233e84b6010000006b483045022100e5374c185ffb992bcabec480572427f402911cdaeda92246f2a94813a32f33ab0220676121e6bd049c981593a8093f779927cc95bdf56d50069b7786ad9b9fe528c84121035d1d732dbe247c0886753c84dc3d2fc96a9eac26662e8664fe9ce8f67ab6dd98ffffffffe5232220aee5069017d31cc30818bcc971de3e6418f6e62b8cc9a3430d64f3e9010000006a473044022048e3532181d848dcf69a4da9b8ae296b90b4660a27f880b928ce15e9d3c5979e02205cef48653b1defbc2a87d93b268693c8d078199b69eb5bba32ba10f301727bf641210343caa07997898400cefe7a28445b233d30463d13359c1d87ac42ea5da61432a0ffffffff01ce070000000000001976a9145c21ae83ea5892dea33b9b002eb1d8450b581ea888ac0000000000", - expectedErr: nil, - expectedErrTxID: "", - }, - { - name: "valid not mined beef", - beefStr: "0100beef03fe4e6d0c001002fd9c67028ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160cfd9d6700db1332728830a58c83a5970dcd111a575a585b43b0492361ea8082f41668f8bd01fdcf3300e568706954aae516ef6df7b5db7828771a1f3fcf1b6d65389ec8be8c46057a3c01fde6190001a6028d13cc988f55c8765e3ffcdcfc7d5185a8ebd68709c0adbe37b528557b01fdf20c001cc64f09a217e1971cabe751b925f246e3c2a8e145c49be7b831eaea3e064d7501fd7806009ccf122626a20cdb054877ef3f8ae2d0503bb7a8704fdb6295b3001b5e8876a101fd3d0300aeea966733175ff60b55bc77edcb83c0fce3453329f51195e5cbc7a874ee47ad01fd9f0100f67f50b53d73ffd6e84c02ee1903074b9a5b2ac64c508f7f26349b73cca9d7e901ce006ce74c7beed0c61c50dda8b578f0c0dc5a393e1f8758af2fb65edf483afcaa68016600e32475e17bdd141d62524d0005989dd1db6ca92c6af70791b0e4802be4c5c8c1013200b88162f494f26cc3a1a4a7dcf2829a295064e93b3dbb2f72e21a73522869277a011800a938d3f80dd25b6a3a80e450403bf7d62a1068e2e4b13f0656c83f764c55bb77010d006feac6e4fea41c37c508b5bfdc00d582f6e462e6754b338c95b448df37bd342c010700bf5448356be23b2b9afe53d00cee047065bbc16d0bbcc5f80aa8c1b509e45678010200c2e37431a437ee311a737aecd3caae1213db353847f33792fd539e380bdb4d440100005d5aef298770e2702448af2ce014f8bfcded5896df5006a44b5f1b6020007aeb01010091484f513003fcdb25f336b9b56dafcb05fbc739593ab573a2c6516b344ca532fede850c000d02fd860c00661459da8afa777590dc3c736af35ded5fc51b602688a3a7c93edaebe6a4652bfd870c029fc71a2f88088174c4d0e0348e7087c9b355a4e93e5b2ec4a5f41fdd69272f8601fd420600a5a336c4022e226e5695521b7ff33ae86afd0f1eeb17ee0dc62501f385fc5b0901fd20030018013a8fbb3176ba39c193948c2c57cec747ec73aa362f52f0720c3391a7c66701fd91010014d30a290a1b63e7e6e16f8e09e2e93a71042462451e1f0b2f9ec8971e0c736a01c900421970e542d08b8b52eb3ba45541226d885dcad9dbe7ccec4111c9cd4c54acb30165005ae426e8c77d1b1153b2425b935ede63f1b98e7852e9eee493415e7bcf2ec56301330039eb742b6167dcb3d9f680e60d13af943f6fff7f6a84dc6ac5680eb32c51e728011800d47cdf86aa745c735cb1de8b5c72dc43d927c58500b76e108b86556845c3aa53010d00725cc42f64c415e1d17803764a441ece296509ecdc77e5962141641df91e276b0107007dd3c756b59f354d81efe05248242d603e4baf5a568b34cfa84b1152c65d2b440102001083fd215308c82b36ab82b7aaf8ef5016edd1f7fe3d7cd0d7c7026d17d6841c010000848831afed0a0552896bf420ad66647af4d0dc8c76d19a6e912a1987ac925dba0101004d4fe46bfd107d575616f76b860ab00906e6ac47250d776a4c2cc67345eef4e8fef3850c000b04fde00502f8aea749791d0907e46bbb7df82dc52f84a9b83434e563e3a6496b95239fc08dfde1050076c8b774bf78f74252cee9698b49fccf87937ae4f2b01b7a87f23b060336585dfd26060295e93ce1a376cfd842ffbd849f0ee37330d6b62a51cddfacfbed9c1a4d49cce7fd270600d232986aa95c1db58e9f56fd3caf52cd812428ffc3fdcdf5be21edea66af1e3802fdf10200eef6a321ca9dcd30df8eb9eeaeacdb0a0535e269279e6ed87c6ab5bfdd14f167fd120300f38cdc9cf336f46aa742e3a1ddd3a50418ca5d52fb2d613a49e9d1f0821fde6c02fd790100e1e3d7d90c3859990c6ea7588f05419fab6eebff078edf3f66a6cd8a87faa7c5fd880100b0f4a9b7219d01ca579ad5e6741fdf677c899f1de9855945f13a286c0452e9b202bd007f0f30d4d1ffc36093444b54cb39693d6c34f4810863850adc824733aeaa55e9c500c37b5a7df0dcc18fe9943b9f2927122d2bd3aaaa8c4bac71d9bbecceee1be782025f00b0126c6c3ede9904454251478bd667b1396678a4567bcbcae0fb33f73e0d7aab6300fe3589fe0e5afc3827d35b1065b7aad956021d388fc6ccb1d5d81657d092b1e2022e00bbcd586527ec559f3ecf031064f631b3c372e7b31d1361be3a9b41bd21a09e84300051af813b88ba08d1526fd4418305a0464f499c51ee1e1bc7231ebbf68d33b812021600e822218713eb86174c91b4cc924dbe8c8e2319309b8195e845e94e0b0760ea191900e8ec605b869f93cf6b1e2d74bcbc9aba1be3d9b4f9bd2e82563fd797b8cd4a3e020a000be8607accf5ae93e80c274bc3e045a3d4e8387b3725dbb1e9583a4ed310ef4b0d0084309da5e0385c79ea670be940324a3416ed96e122a24aca17773cb5899b407b02040036bd04fa34848574dc1665ba52bbe49038b9595635fa4b525d7de6ef9c8988210700c4c2ef77377913248dd9fe8e659c6f0f80b9b72fa98ffed670f8fadc5c91f3930202007a3e5caea9497ee7595c8aa9bcb6a08c2d06e215154085f224345aaf3ede4e870300e9bb93416e69b38eacd6aba4258afde7a007ac52dcaa28aabd65aad90ac3bc1b010000c1006f88da5f5841323cd779a19689a43d9311785be06efb22ad98c3cc6be1260901000000029ceda549c273cb093678c42c5765354c735c005cc1feb72e1a291f6bf3b3e312000000006a47304402204c992ff3b4185f9858e72e0b3d01d6d4ed144e363c13afe662c8edf37744e612022018fcd7e2492e365f738fb07fa5c1dfa84ab9f49469559f5540be1e115104066b4121031499e4d192d8e2b2409cb757608bee58701cf6d58d55cff97fa584671bce4728fffffffffc67309c8602eb6a14b805c27641337f114314ac09fa96b2974d73d11997639d010000006a473044022069eeddbc83b657ea6f40d1c6ae535a7993943ce5624494d6069b64fc45d17cef02202346f9131101d94bd15b0fb122f25fbc41e6d0b941b42eb79549c7cfb78e5ac441210242cf3c106badb9695cbf7c64039bce81b0a58cbf89b3a04d90577bc801b2a9efffffffff03e8030000000000001976a914ff98769820cd542ba0c6fc8f0db003ef51a6f84d88ac1f070000000000001976a914e11d3852660e0630189bfd178ed7652fb0be25de88acb8260000000000001976a9141fbd24e6a4b10ce1f04775d0e8e42c4406e3824b88ac00000000010101000000027b0a1b12c7c9e48015e78d3a08a4d62e439387df7e0d7a810ebd4af37661daaa000000006a47304402207d972759afba7c0ffa6cfbbf39a31c2aeede1dae28d8841db56c6dd1197d56a20220076a390948c235ba8e72b8e43a7b4d4119f1a81a77032aa6e7b7a51be5e13845412103f78ec31cf94ca8d75fb1333ad9fc884e2d489422034a1efc9d66a3b72eddca0fffffffff7f36874f858fb43ffcf4f9e3047825619bad0e92d4b9ad4ba5111d1101cbddfe010000006a473044022043f048043d56eb6f75024808b78f18808b7ab45609e4c4c319e3a27f8246fc3002204b67766b62f58bf6f30ea608eaba76b8524ed49f67a90f80ac08a9b96a6922cd41210254a583c1c51a06e10fab79ddf922915da5f5c1791ef87739f40cb68638397248ffffffff03e8030000000000001976a914b08f70bc5010fb026de018f19e7792385a146b4a88acf3010000000000001976a9147d48635f889372c3da12d75ce246c59f4ab907ed88acf7000000000000001976a914b8fbd58685b6920d8f9a8f1b274d8696708b51b088ac00000000010001000000028ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160c000000006b483045022100c5db842085740c8467a6755693e7048b2f4134371a0f01509ee1db9b7274403f022064de309ec9fbdca4ca1ab220cd35d6efd04028217f535cc43649f96250bb53154121028fd1afeee81361e801800afb264e35cdce3037ec6f7dc4f1d1eaba7ad519c948ffffffff9fc71a2f88088174c4d0e0348e7087c9b355a4e93e5b2ec4a5f41fdd69272f86000000006b4830450221009bc74c7331b010f8befcc4a7ee630876282bee47775f04796567dc875ec83c05022064b558766a1f8badc0c26c0b3d7bcde99cf9d774f88a87f8f45d440ca54043a241210336abf8512542305fa13788dae2e4e9f0fb2ae5dc57cb1ced980eaee6e88f343fffffffff02dc050000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88acf3010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac00000000000100000002418eb1b58f47af50a079e1c695e6ce86f79c092caf050128f0d7785381d960d3010000006a47304402207b019f395fdedfe3632433df49eba13e33d8c8f0328834eb0366635853c3e8ff0220505ef2df0702e918897019e49a486ecf8238b179d102cc3c0920564dd6a84d064121034e92b3e62017ee88487a69f8ecda9d17b4daf590d6e06362a4139b1d993dd044fffffffffea6861820d117e799caf1553ce075435f3ea153aff26c9551c71047be4089bb020000006a47304402207b58b596f1094c3e0570f136f1c86ae883a260b6fe671b07ccda85136561bab80220410924b50aa6a7ba9c864f8d206f1bba2c7b384127984e2a1e9db066f82379ca41210301b1d251f89768ff33a01c192a400de69573bdfc44fa67fde13eb259eb81ea64ffffffff03f4010000000000001976a914601d40068f463ce0124e1b18466e1bd7d6b333a388acf4010000000000001976a914668c08cab622e65e3aaa1c1365b8e4c029a6365688acb9260000000000001976a914e861b54724209a7eb9b0512f31c50c1ae2a07cb588ac000000000102010000000195e48d7d5679e0740c85f3a9e1e69a9f5c8493f7ec7c454b0193c15dcc7007cd010000006a47304402205babaf7fb8cc6e9347c0cc743c9f044bf8a3820f05fdc57a3591d4cf2df86bc402202e816d35e9a937cffe8cde4f68ee64b06fc5e070335c0c2262fdf016eaca47b94121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff0264000000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac8e010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac000000000001000000029fc71a2f88088174c4d0e0348e7087c9b355a4e93e5b2ec4a5f41fdd69272f86010000006a47304402203a5c238e8ceccff7b7db05a4dba524f6694599014dbac5ae92dc0c62635c8763022078cab36b04a16eeead80fa49dd4d1c22f14d8084c78977917cfe76fee9d95a84412102993285bd72def003516c414b4fb3c3718d5d003c90f08c75bd6eecc08910585fffffffff396915fa4cff57573f0e80b1d585d5a4f7692984e51577c8ea82e1d88f8404ce060000006a47304402205ba90f2ed3f70c779771185a473e3b1844ad0f8f86bddf39148ad277c9a2298902203df352b710b05501cc3d773e366854770038f9dea65c046b666fd28b124f303d412103c335fc77d28a0181bec42cd7afda547a4d524c5b1f04a7d9d0ec76f1217a0e5affffffff03f4010000000000001976a91494c77286c0552398575ea741337dc6143c7978ec88ac2b050000000000001976a91418d9eaafcf7d8e8ba29ac102ed19b9b3808376a088ac0f4e0000000000001976a914555b7d05b5327c78c7bf7f8748c1b3ce3552168888ac0000000001020100000001f8aea749791d0907e46bbb7df82dc52f84a9b83434e563e3a6496b95239fc08d000000006b483045022100a50ebc1429285edf0d39d77f0ab49e68eaf6e8f97d727671fcc51763235ca39a02200252a185b70fb5ac893ad32e8ffe1e8ade9d3d8993f8db4a8cb27dc6e32c18664121037b231d9fdd2f7a41181de9a14ae11a10b7332b1fdb5526c1a0060eb964f111d6ffffffff0296000000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac5d010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac0000000000010000000183d98243af506e20461097054c6692a09502ee7a99eba67cd20b0d5b66d081f0010000006a473044022055e323fc40f05fdeb836fadf19003d91f08abef97526e04f4c976156faedc83c022050f36b4d23329fa9d6f37bfee952ae3a0bf266573fc19a51ab42595ee674a26a4121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff0264000000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac29010000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac000000000001000000032a796907de322523a34e801e304ea43b6938cfa5afe3ff4fea7fbc34bd52164c010000006b483045022100ec1d2ab4bedca3a696a6f0589b65015b07d5a00b7517d03f783050323165b16902200c18cf37ba54454f0e28e610ec14e53be4cfc254182142e111c2571c3432f15d4121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff73bd2e109d1e01d9c5828e15b7e3212e54dd3d9558ceae6514c97e07ad275110010000006b48304502210081d8eb51e97f6c50460a3f526f14f35f8882f51f9455c7b06f63a3aedd16c057022019e8014a1c2bed2b4f9c0e15989056a53400ae3e3ed1114f516fa9b6390af7154121033733254e143eba5ee6989fe898647d4e6170c0d82faa72fbce1c1e701960edb9ffffffff95e93ce1a376cfd842ffbd849f0ee37330d6b62a51cddfacfbed9c1a4d49cce7000000006a47304402202b185fd9848a68d76167383535de9d8adcdb2e219e8210641e4aead624532d3e02206f2f24fa29a81f73438a3cf7208bd021fb9fa8c1f30376b00af6bf7766b29d7e41210275aaa422bc13c303b70026b3fd1fff37c199e5e98bd0a429922217f9b861870effffffff024c040000000000001976a914ddfbaa2cd75b86cf24135603b5ddd2b1968bc62d88ac2d000000000000001976a9148360b65a14e83a68aef2fae3f9cf472a90dd4da488ac0000000000", - expectedErr: nil, - expectedErrTxID: "", - }, - { - name: "invalid beef - low fees", - beefStr: "0100beef01fe4e6d0c001002fd9c67028ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160cfd9d6700db1332728830a58c83a5970dcd111a575a585b43b0492361ea8082f41668f8bd01fdcf3300e568706954aae516ef6df7b5db7828771a1f3fcf1b6d65389ec8be8c46057a3c01fde6190001a6028d13cc988f55c8765e3ffcdcfc7d5185a8ebd68709c0adbe37b528557b01fdf20c001cc64f09a217e1971cabe751b925f246e3c2a8e145c49be7b831eaea3e064d7501fd7806009ccf122626a20cdb054877ef3f8ae2d0503bb7a8704fdb6295b3001b5e8876a101fd3d0300aeea966733175ff60b55bc77edcb83c0fce3453329f51195e5cbc7a874ee47ad01fd9f0100f67f50b53d73ffd6e84c02ee1903074b9a5b2ac64c508f7f26349b73cca9d7e901ce006ce74c7beed0c61c50dda8b578f0c0dc5a393e1f8758af2fb65edf483afcaa68016600e32475e17bdd141d62524d0005989dd1db6ca92c6af70791b0e4802be4c5c8c1013200b88162f494f26cc3a1a4a7dcf2829a295064e93b3dbb2f72e21a73522869277a011800a938d3f80dd25b6a3a80e450403bf7d62a1068e2e4b13f0656c83f764c55bb77010d006feac6e4fea41c37c508b5bfdc00d582f6e462e6754b338c95b448df37bd342c010700bf5448356be23b2b9afe53d00cee047065bbc16d0bbcc5f80aa8c1b509e45678010200c2e37431a437ee311a737aecd3caae1213db353847f33792fd539e380bdb4d440100005d5aef298770e2702448af2ce014f8bfcded5896df5006a44b5f1b6020007aeb01010091484f513003fcdb25f336b9b56dafcb05fbc739593ab573a2c6516b344ca5320201000000027b0a1b12c7c9e48015e78d3a08a4d62e439387df7e0d7a810ebd4af37661daaa000000006a47304402207d972759afba7c0ffa6cfbbf39a31c2aeede1dae28d8841db56c6dd1197d56a20220076a390948c235ba8e72b8e43a7b4d4119f1a81a77032aa6e7b7a51be5e13845412103f78ec31cf94ca8d75fb1333ad9fc884e2d489422034a1efc9d66a3b72eddca0fffffffff7f36874f858fb43ffcf4f9e3047825619bad0e92d4b9ad4ba5111d1101cbddfe010000006a473044022043f048043d56eb6f75024808b78f18808b7ab45609e4c4c319e3a27f8246fc3002204b67766b62f58bf6f30ea608eaba76b8524ed49f67a90f80ac08a9b96a6922cd41210254a583c1c51a06e10fab79ddf922915da5f5c1791ef87739f40cb68638397248ffffffff03e8030000000000001976a914b08f70bc5010fb026de018f19e7792385a146b4a88acf3010000000000001976a9147d48635f889372c3da12d75ce246c59f4ab907ed88acf7000000000000001976a914b8fbd58685b6920d8f9a8f1b274d8696708b51b088ac00000000010001000000018ae36502fdc82837319362c488fb9cb978e064daf600bbfc48389663fc5c160c000000006b483045022100e47fbd96b59e2c22be273dcacea74a4be568b3e61da7eddddb6ce43d459c4cf202201a580f3d9442d5dce3f2ced03256ca147bcd230975a6067954e22415715f4490412102b0c8980f5d2cab77c92c68ac46442feba163a9d306913f6a34911fc618c3c4e7ffffffff0188130000000000001976a9148a8c4546a95e6fc8d18076a9980d59fd882b4e6988ac0000000000", - expectedErr: validator.NewError(errors.New("transaction fee of 0 sat is too low - minimum expected fee is 1 sat"), api.ErrStatusFees), - expectedErrTxID: "8184849de6af441c7de088428073c2a9131b08f7d878d9f49c3faf6d941eb168", - }, - { - name: "invlid beef - invalid scripts", - beefStr: "0100beef01fe4e6d0c001002fd909002088a382ec07a8cf47c6158b68e5822852362102d8571482d1257e0b7527e1882fd91900065cb01218f2506bb51155d243e4d6b32d69d1b5f2221c52e26963cfd8cf7283201fd4948008d7a44ae384797b0ae84db0c857e8c1083425d64d09ef8bc5e2e9d270677260501fd25240060f38aa33631c8d70adbac1213e7a5b418c90414e919e3a12ced63dd152fd85a01fd1312005ff132ee64a7a0c79150a29f66ef861e552d3a05b47d6303f5d8a2b2a09bc61501fd080900cc0baf21cf06b9439dfe05dce9bdb14ddc2ca2d560b1138296ef5769851a84b301fd85040063ccb26232a6e1d3becdb47a0f19a67a562b754e8894155b3ae7bba10335ce5101fd430200e153fc455a0f2c8372885c11af70af904dcf44740b9ebf3b3e5b2234cce550bc01fd20010077d5ea69d1dcc379dde65d6adcebde1838190118a8fae928c037275e78bd87910191000263e4f31684a25169857f2788aeef603504931f92585f02c4c9e023b2aa43d1014900de72292e0b3e5eeacfa2b657bf4d46c885559b081ee78632a99b318c1148d85c01250068a5f831ca99b9e7f3720920d6ea977fd2ab52b83d1a6567dafa4c8cafd941ed0113006a0b91d83f9056b702d6a8056af6365c7da626fc3818b815dd4b0de22d05450f0108009876ce56b68545a75859e93d200bdde7880d46f39384818b259ed847a9664ddf010500990bc5e95cacbc927b5786ec39a183f983fe160d52829cf47521c7eb369771c30103004fe794e50305f590b6010a51d050bf47dfeaabfdb949c5ee0673f577a59537d70100004dad44a358aea4d8bc1917912539901f5ae44e07a4748e1a9d3018814b0759d0020100000002704273c86298166ac351c3aa9ac90a8029e4213b5f1b03c3bbf4bc5fb09cdd43010000006a4730440220398d6389e8a156a3c6c1ca355e446d844fd480193a93af832afd1c87d0f04784022050091076b8f7405b37ce6e795d1b92526396ac2b14f08e91649b908e711e2b044121030ef6975d46dbab4b632ef62fdbe97de56d183be1acc0be641d2c400ae01cf136ffffffff2f41ed6a2488ac3ba4a3c330a15fa8193af87f0192aa59935e6c6401d92dc3a00a0000006a47304402200ad9cf0dc9c90a4c58b08910740b4a8b3e1a7e37db1bc5f656361b93f412883d0220380b6b3d587103fc8bf3fe7bed19ab375766984c67ebb7d43c993bcd199f32a441210205ef4171f58213b5a2ddf16ac6038c10a2a8c3edc1e6275cb943af4bb3a58182ffffffff03e8030000000000001976a9148a8c4546a95e6fc8d18076a9980d59fd882b4e6988acf4010000000000001976a914c7662da5e0a6a179141a7872045538126f1e954288acf5000000000000001976a914765bdf10934f5aac894cf8a3795c9eeb494c013488ac0000000001000100000001088a382ec07a8cf47c6158b68e5822852362102d8571482d1257e0b7527e1882000000006a4730440220610bba9ed83a47641c34bbbcf8eeb536d2ae6cfddc7644a8c520bb747f798c3702206a23c9f45273772dd7e80ba21a5c4613d6ffe7ba1c75b729eae0cdd484fee2bd412103c0cd91af135d09f98d57e34af28e307daf36bccd4764708e8a3f7ea5cebf01a9ffffffff01c8000000000000001976a9148ce2d21f9a75e98600be76b25b91c4fef6b40bcd88ac0000000000", - expectedErr: validator.NewError(errors.New("invalid script"), api.ErrStatusUnlockingScripts), - expectedErrTxID: "ea2924da32c47b9942cda5ad30d3c01610ca554ca3a9ca01b2ccfe72bf0667be", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - beefHex, err := hex.DecodeString(tc.beefStr) - require.NoError(t, err) - - beef, _, err := beef.DecodeBEEF(beefHex) - require.NoError(t, err) - - policy := getPolicy(1) - validator := New(policy) - - errTx, err := validator.ValidateBeef(beef, false, false) - assert.Equal(t, tc.expectedErr, err) - if tc.expectedErrTxID != "" { - assert.NotNil(t, errTx) - assert.Equal(t, tc.expectedErrTxID, errTx.TxID()) - } - }) - } -} - func getPolicy(satoshisPerKB uint64) *bitcoin.Settings { var policy *bitcoin.Settings @@ -198,286 +134,8 @@ func getPolicy(satoshisPerKB uint64) *bitcoin.Settings { return policy } -// getTx is a helper function to get a transaction from the bitcoin node -func _(txID string) (*bt.Tx, error) { - b, err := bitcoin.New("localhost", 8332, "bitcoin", "bitcoin", false) - if err != nil { - return nil, err - } - - var hexStr *string - if hexStr, err = b.GetRawTransactionHex(txID); err != nil { - return nil, err - } - - return bt.NewTxFromString(*hexStr) -} - -func Test_checkTxSize(t *testing.T) { - type args struct { - txSize int - policy *bitcoin.Settings - } - - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "valid tx size", - args: args{ - txSize: 100, - policy: &bitcoin.Settings{ - MaxTxSizePolicy: 10000000, - }, - }, - wantErr: false, - }, - { - name: "invalid tx size", - args: args{ - txSize: MaxBlockSize + 1, - policy: &bitcoin.Settings{ - MaxTxSizePolicy: 10000000, - }, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := checkTxSize(tt.args.txSize, tt.args.policy); (err != nil) != tt.wantErr { - t.Errorf("checkTxSize() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -//nolint:funlen - don't need to check length of test functions -func Test_checkOutputs(t *testing.T) { - type args struct { - tx *bt.Tx - } - - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "valid output", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{{Satoshis: 100, LockingScript: validLockingScript}}, - }, - }, - wantErr: false, - }, - { - name: "invalid satoshis > max", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{{Satoshis: MaxSatoshis + 1, LockingScript: validLockingScript}}, - }, - }, - wantErr: true, - }, - { - name: "invalid satoshis == 0", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{{Satoshis: 0, LockingScript: validLockingScript}}, - }, - }, - wantErr: true, - }, - { - name: "valid satoshis == 0, op return", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{{Satoshis: 0, LockingScript: opReturnLockingScript}}, - }, - }, - wantErr: false, - }, - { - name: "invalid satoshis, op return", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{{Satoshis: 100, LockingScript: opReturnLockingScript}}, - }, - }, - wantErr: true, - }, - { - name: "invalid total satoshis", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{ - {Satoshis: MaxSatoshis - 100, LockingScript: validLockingScript}, - {Satoshis: MaxSatoshis - 100, LockingScript: validLockingScript}, - }, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := checkOutputs(tt.args.tx); (err != nil) != tt.wantErr { - t.Errorf("checkOutputs() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_checkInputs(t *testing.T) { - type args struct { - tx *bt.Tx - } - - coinbaseInput := &bt.Input{} - coinbaseInput.PreviousTxSatoshis = 100 - _ = coinbaseInput.PreviousTxIDAddStr(coinbaseTxID) - - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "invalid coinbase input", - args: args{ - tx: &bt.Tx{Inputs: []*bt.Input{coinbaseInput}}, - }, - wantErr: true, - }, - { - name: "valid input", - args: args{ - tx: &bt.Tx{Inputs: []*bt.Input{{PreviousTxSatoshis: 100}}}, - }, - wantErr: false, - }, - { - name: "invalid input satoshis", - args: args{ - tx: &bt.Tx{Inputs: []*bt.Input{{PreviousTxSatoshis: MaxSatoshis + 1}}}, - }, - wantErr: true, - }, - { - name: "invalid input satoshis", - args: args{ - tx: &bt.Tx{ - Inputs: []*bt.Input{{ - PreviousTxSatoshis: MaxSatoshis - 100, - }, { - PreviousTxSatoshis: MaxSatoshis - 100, - }}, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := checkInputs(tt.args.tx); (err != nil) != tt.wantErr { - t.Errorf("checkInputs() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_calculateFeesRequired(t *testing.T) { - defaultFees := bt.NewFeeQuote() - for _, feeType := range []bt.FeeType{bt.FeeTypeStandard, bt.FeeTypeData} { - defaultFees.AddQuote(feeType, &bt.Fee{ - MiningFee: bt.FeeUnit{ - Satoshis: 1, - Bytes: 1000, - }, - }) - } - - tt := []struct { - name string - fees *bt.FeeQuote - size *bt.TxSize - - expectedRequiredMiningFee uint64 - }{ - { - name: "1.311 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 1111}, - - expectedRequiredMiningFee: 1, - }, - { - name: "1.861 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 50, TotalDataBytes: 1811}, - - expectedRequiredMiningFee: 2, - }, - { - name: "13.31 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 13110}, - - expectedRequiredMiningFee: 13, - }, - { - name: "18.71 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 18510}, - - expectedRequiredMiningFee: 19, - }, - { - name: "1.5 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 1300}, - - expectedRequiredMiningFee: 2, - }, - { - name: "0.8 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 600}, - - expectedRequiredMiningFee: 1, - }, - { - name: "0.5 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 300}, - - expectedRequiredMiningFee: 1, - }, - { - name: "0.2 kb size", - fees: defaultFees, - size: &bt.TxSize{TotalStdBytes: 100, TotalDataBytes: 100}, - - expectedRequiredMiningFee: 1, - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - requiredMiningFee, err := calculateMiningFeesRequired(tc.size, tc.fees) - require.NoError(t, err) - require.Equal(t, tc.expectedRequiredMiningFee, requiredMiningFee) - }) - } -} - // no need to extensively test this function, it's just calling bt.IsFeePaidEnough -func Test_checkFees(t *testing.T) { +func Test_standardCheckFees(t *testing.T) { type args struct { tx *bt.Tx feeQuote *bt.FeeQuote @@ -522,140 +180,25 @@ func Test_checkFees(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := checkFees(tt.args.tx, tt.args.feeQuote); (err != nil) != tt.wantErr { - t.Errorf("checkFees() error = %v, wantErr %v", err, tt.wantErr) + if err := standardCheckFees(tt.args.tx, tt.args.feeQuote); (err != nil) != tt.wantErr { + t.Errorf("standardCheckFees() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func Test_checkFeesTxs(t *testing.T) { +func Test_standardCheckFeesTxs(t *testing.T) { t.Run("no fee being paid", func(t *testing.T) { tx, err := bt.NewTxFromString(opReturnTx) require.NoError(t, err) feeQuote := bt.NewFeeQuote() setFees(feeQuote, 50) - err = checkFees(tx, feeQuote) + err = standardCheckFees(tx, feeQuote) require.NoError(t, err) }) } -func Test_sigOpsCheck(t *testing.T) { - type args struct { - tx *bt.Tx - policy *bitcoin.Settings - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "valid sigops", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{{LockingScript: validLockingScript}}, - }, - policy: &bitcoin.Settings{ - MaxTxSigopsCountsPolicy: 4294967295, - }, - }, - wantErr: false, - }, - { - name: "invalid sigops - too many sigops", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{ - { - LockingScript: validLockingScript, - }, - { - LockingScript: validLockingScript, - }, - }, - }, - policy: &bitcoin.Settings{ - MaxTxSigopsCountsPolicy: 1, - }, - }, - wantErr: true, - }, - { - name: "valid sigops - default policy", - args: args{ - tx: &bt.Tx{ - Outputs: []*bt.Output{ - { - LockingScript: validLockingScript, - }, - { - LockingScript: validLockingScript, - }, - }, - }, - policy: &bitcoin.Settings{ - MaxTxSigopsCountsPolicy: 4294967295, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := sigOpsCheck(tt.args.tx, tt.args.policy); (err != nil) != tt.wantErr { - t.Errorf("sigOpsCheck() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_pushDataCheck(t *testing.T) { - type args struct { - tx *bt.Tx - } - - validUnlockingBytes, _ := hex.DecodeString("4730440220318d23e6fd7dd5ace6e8dc1888b363a053552f48ecc166403a1cc65db5e16aca02203a9ad254cb262f50c89487ffd72e8ddd8536c07f4b230d13a2ccd1435898e89b412102dd7dce95e52345704bbb4df4e4cfed1f8eaabf8260d33597670e3d232c491089") - validUnlockingScript := bscript.Script(validUnlockingBytes) - - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "valid push data", - args: args{ - tx: &bt.Tx{ - Inputs: []*bt.Input{{ - UnlockingScript: &validUnlockingScript, - }}, - }, - }, - wantErr: false, - }, - { - name: "invalid push data", - args: args{ - tx: &bt.Tx{ - Inputs: []*bt.Input{{ - UnlockingScript: validLockingScript, - }}, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := pushDataCheck(tt.args.tx); (err != nil) != tt.wantErr { - t.Errorf("pushDataCheck() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func Test_checkScripts(t *testing.T) { t.Run("valid op_return tx", func(t *testing.T) { tx, err := bt.NewTxFromString(opReturnTx) @@ -707,7 +250,7 @@ func BenchmarkValidator(b *testing.B) { validator := New(policy) for i := 0; i < b.N; i++ { - _ = validator.ValidateEFTransaction(tx, false, false) + _ = validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) } } @@ -718,57 +261,6 @@ func TestFeeCalculation(t *testing.T) { policy := getPolicy(50) validator := New(policy) - err = validator.ValidateEFTransaction(tx, false, false) + err = validator.ValidateTransaction(context.TODO(), tx, validation.StandardFeeValidation, validation.StandardScriptValidation) t.Log(err) } - -// func extendTransaction(transaction *bt.Tx) (err error) { -// parentTxBytes := make(map[string][]byte) -// var btParentTx *bt.Tx - -// url, err := url.Parse("http://bitcoin:bitcoin@localhost:8332") -// if err != nil { -// return err -// } - -// bb, err := bitcoin.NewFromURL(url, false) -// if err != nil { -// return err -// } - -// // get the missing input data for the transaction -// for _, input := range transaction.Inputs { -// parentTxIDStr := input.PreviousTxIDStr() -// b, ok := parentTxBytes[parentTxIDStr] -// if !ok { -// p, err := bb.GetRawTransactionHex(parentTxIDStr) -// if err != nil { -// return err -// } - -// ptx, err := bt.NewTxFromString(*p) -// if err != nil { -// return err -// } - -// parentTxBytes[parentTxIDStr] = ptx.Bytes() - -// b = ptx.Bytes() -// } - -// btParentTx, err = bt.NewTxFromBytes(b) -// if err != nil { -// return err -// } - -// if len(btParentTx.Outputs) < int(input.PreviousTxOutIndex) { -// return fmt.Errorf("output %d not found in transaction %s", input.PreviousTxOutIndex, parentTxIDStr) -// } -// output := btParentTx.Outputs[input.PreviousTxOutIndex] - -// input.PreviousTxScript = output.LockingScript -// input.PreviousTxSatoshis = output.Satoshis -// } - -// return nil -// } diff --git a/internal/validator/validation.go b/internal/validator/validation.go new file mode 100644 index 000000000..2ebbbb7db --- /dev/null +++ b/internal/validator/validation.go @@ -0,0 +1,237 @@ +package validator + +import ( + "encoding/hex" + "fmt" + "math" + + "github.com/bitcoin-sv/arc/pkg/api" + "github.com/libsv/go-bt/bscript" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript/interpreter" + "github.com/ordishs/go-bitcoin" +) + +// maxBlockSize is set dynamically in a node, and should be gotten from the policy +const ( + maxBlockSize = 4 * 1024 * 1024 * 1024 + maxSatoshis = 21_000_000_00_000_000 + coinbaseTxID = "0000000000000000000000000000000000000000000000000000000000000000" + maxTxSigopsCountPolicyAfterGenesis = ^uint32(0) // UINT32_MAX + minTxSizeBytes = 61 +) + +func CalculateMiningFeesRequired(size *bt.TxSize, fees *bt.FeeQuote) (uint64, error) { + var feesRequired float64 + + feeStandard, err := fees.Fee(bt.FeeTypeStandard) + if err != nil { + return 0, err + } + + feesRequired += float64(size.TotalStdBytes) * float64(feeStandard.MiningFee.Satoshis) / float64(feeStandard.MiningFee.Bytes) + + feeData, err := fees.Fee(bt.FeeTypeData) + if err != nil { + return 0, err + } + + feesRequired += float64(size.TotalDataBytes) * float64(feeData.MiningFee.Satoshis) / float64(feeData.MiningFee.Bytes) + + // the minimum fees required is 1 satoshi + feesRequiredRounded := uint64(math.Round(feesRequired)) + if feesRequiredRounded < 1 { + feesRequiredRounded = 1 + } + + return feesRequiredRounded, nil +} + +func CommonValidateTransaction(policy *bitcoin.Settings, tx *bt.Tx) error { + // + // Each node will verify every transaction against a long checklist of criteria: + // + txSize := tx.Size() + + // 1) Neither lists of inputs or outputs are empty + if len(tx.Inputs) == 0 || len(tx.Outputs) == 0 { + return NewError(fmt.Errorf("transaction has no inputs or outputs"), api.ErrStatusInputs) + } + + // 2) The transaction size in bytes is less than maxtxsizepolicy. + if err := checkTxSize(txSize, policy); err != nil { + return NewError(err, api.ErrStatusTxFormat) + } + + // 3) check that each input value, as well as the sum, are in the allowed range of values (less than 21m coins) + // 5) None of the inputs have hash=0, N=–1 (coinbase transactions should not be relayed) + if err := checkInputs(tx); err != nil { + return NewError(err, api.ErrStatusInputs) + } + + // 4) Each output value, as well as the total, must be within the allowed range of values (less than 21m coins, + // more than the dust threshold if 1 unless it's OP_RETURN, which is allowed to be 0) + if err := checkOutputs(tx); err != nil { + return NewError(err, api.ErrStatusOutputs) + } + + // 6) nLocktime is equal to INT_MAX, or nLocktime and nSequence values are satisfied according to MedianTimePast + // => checked by the node, we do not want to have to know the current block height + + // 7) The transaction size in bytes is greater than or equal to 100 + if txSize < minTxSizeBytes { + return NewError(fmt.Errorf("transaction size in bytes is less than %d bytes", minTxSizeBytes), api.ErrStatusMalformed) + } + + // 8) The number of signature operations (SIGOPS) contained in the transaction is less than the signature operation limit + if err := sigOpsCheck(tx, policy); err != nil { + return NewError(err, api.ErrStatusMalformed) + } + + // 9) The unlocking script (scriptSig) can only push numbers on the stack + if err := pushDataCheck(tx); err != nil { + return NewError(err, api.ErrStatusMalformed) + } + + // // 10) Reject if the sum of input values is less than sum of output values + // // 11) Reject if transaction fee would be too low (minRelayTxFee) to get into an empty block. + // switch feeValidation { + // case StandardFeeValidation: + // if err := checkFees(tx, api.FeesToBtFeeQuote(policy.MinMiningTxFee)); err != nil { + // return NewError(err, api.ErrStatusFees) + // } + // case DeepFeeValidation: + // if err := v.deepCheckFees(ctx, tx, api.FeesToBtFeeQuote(policy.MinMiningTxFee)); err != nil { + // return NewError(err, api.ErrStatusFees) // TODO: add new status + // } + // default: + // } + + // // 12) The unlocking scripts for each input must validate against the corresponding output locking scripts + // if scriptValidation == StandardScriptValidation { + // if err := checkScripts(tx); err != nil { + // return NewError(err, api.ErrStatusUnlockingScripts) + // } + // } + + // everything checks out + return nil +} + +func checkTxSize(txSize int, policy *bitcoin.Settings) error { + maxTxSizePolicy := policy.MaxTxSizePolicy + if maxTxSizePolicy == 0 { + // no policy found for tx size, use max block size + maxTxSizePolicy = maxBlockSize + } + if txSize > maxTxSizePolicy { + return fmt.Errorf("transaction size in bytes is greater than max tx size policy %d", maxTxSizePolicy) + } + + return nil +} + +func checkOutputs(tx *bt.Tx) error { + total := uint64(0) + for index, output := range tx.Outputs { + isData := output.LockingScript.IsData() + switch { + case !isData && (output.Satoshis > maxSatoshis || output.Satoshis < bt.DustLimit): + return NewError(fmt.Errorf("transaction output %d satoshis is invalid", index), api.ErrStatusOutputs) + case isData && output.Satoshis != 0: + return NewError(fmt.Errorf("transaction output %d has non 0 value op return", index), api.ErrStatusOutputs) + } + total += output.Satoshis + } + + if total > maxSatoshis { + return NewError(fmt.Errorf("transaction output total satoshis is too high"), api.ErrStatusOutputs) + } + + return nil +} + +func checkInputs(tx *bt.Tx) error { + total := uint64(0) + for index, input := range tx.Inputs { + if hex.EncodeToString(input.PreviousTxID()) == coinbaseTxID { + return NewError(fmt.Errorf("transaction input %d is a coinbase input", index), api.ErrStatusInputs) + } + /* lots of our valid test transactions have this sequence number, is this not allowed? + if input.SequenceNumber == 0xffffffff { + fmt.Printf("input %d has sequence number 0xffffffff, txid = %s", index, tx.TxID()) + return validator.NewError(fmt.Errorf("transaction input %d sequence number is invalid", index), arc.ErrStatusInputs) + } + */ + if input.PreviousTxSatoshis > maxSatoshis { + return NewError(fmt.Errorf("transaction input %d satoshis is too high", index), api.ErrStatusInputs) + } + total += input.PreviousTxSatoshis + } + if total > maxSatoshis { + return NewError(fmt.Errorf("transaction input total satoshis is too high"), api.ErrStatusInputs) + } + + return nil +} + +func sigOpsCheck(tx *bt.Tx, policy *bitcoin.Settings) error { + maxSigOps := policy.MaxTxSigopsCountsPolicy + + if maxSigOps == 0 { + maxSigOps = int64(maxTxSigopsCountPolicyAfterGenesis) + } + + parser := interpreter.DefaultOpcodeParser{} + numSigOps := int64(0) + + for _, input := range tx.Inputs { + parsedUnlockingScript, err := parser.Parse(input.UnlockingScript) + if err != nil { + return err + } + + for _, op := range parsedUnlockingScript { + if op.Value() == bscript.OpCHECKSIG || op.Value() == bscript.OpCHECKSIGVERIFY { + numSigOps++ + } + } + } + + for _, output := range tx.Outputs { + parsedLockingScript, err := parser.Parse(output.LockingScript) + if err != nil { + return err + } + + for _, op := range parsedLockingScript { + if op.Value() == bscript.OpCHECKSIG || op.Value() == bscript.OpCHECKSIGVERIFY { + numSigOps++ + } + } + } + + if numSigOps > maxSigOps { + return fmt.Errorf("transaction unlocking scripts have too many sigops (%d)", numSigOps) + } + + return nil +} + +func pushDataCheck(tx *bt.Tx) error { + for index, input := range tx.Inputs { + if input.UnlockingScript == nil { + return fmt.Errorf("transaction input %d unlocking script is empty", index) + } + parser := interpreter.DefaultOpcodeParser{} + parsedUnlockingScript, err := parser.Parse(input.UnlockingScript) + if err != nil { + return err + } + if !parsedUnlockingScript.IsPushOnly() { + return fmt.Errorf("transaction input %d unlocking script is not push only", index) + } + } + + return nil +} diff --git a/internal/validator/validation_test.go b/internal/validator/validation_test.go new file mode 100644 index 000000000..96c240d89 --- /dev/null +++ b/internal/validator/validation_test.go @@ -0,0 +1,397 @@ +package validator + +import ( + "encoding/hex" + "testing" + + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript" + "github.com/ordishs/go-bitcoin" + "github.com/stretchr/testify/require" +) + +var validLockingScript = &bscript.Script{ + 0x76, 0xa9, 0x14, 0xcd, 0x43, 0xba, 0x65, 0xce, 0x83, 0x77, 0x8e, 0xf0, 0x4b, 0x20, 0x7d, 0xe1, 0x44, 0x98, 0x44, 0x0f, 0x3b, 0xd4, 0x6c, 0x88, 0xac, +} + +var opReturnLockingScript = &bscript.Script{ + 0x00, 0x6a, 0x4c, 0x4d, 0x41, 0x50, 0x49, 0x20, 0x30, 0x2e, 0x31, 0x2e, 0x30, 0x20, 0x2d, 0x20, +} + +func Test_calculateFeesRequired(t *testing.T) { + defaultFees := bt.NewFeeQuote() + for _, feeType := range []bt.FeeType{bt.FeeTypeStandard, bt.FeeTypeData} { + defaultFees.AddQuote(feeType, &bt.Fee{ + MiningFee: bt.FeeUnit{ + Satoshis: 1, + Bytes: 1000, + }, + }) + } + + tt := []struct { + name string + fees *bt.FeeQuote + size *bt.TxSize + + expectedRequiredMiningFee uint64 + }{ + { + name: "1.311 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 1111}, + + expectedRequiredMiningFee: 1, + }, + { + name: "1.861 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 50, TotalDataBytes: 1811}, + + expectedRequiredMiningFee: 2, + }, + { + name: "13.31 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 13110}, + + expectedRequiredMiningFee: 13, + }, + { + name: "18.71 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 18510}, + + expectedRequiredMiningFee: 19, + }, + { + name: "1.5 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 1300}, + + expectedRequiredMiningFee: 2, + }, + { + name: "0.8 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 600}, + + expectedRequiredMiningFee: 1, + }, + { + name: "0.5 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 200, TotalDataBytes: 300}, + + expectedRequiredMiningFee: 1, + }, + { + name: "0.2 kb size", + fees: defaultFees, + size: &bt.TxSize{TotalStdBytes: 100, TotalDataBytes: 100}, + + expectedRequiredMiningFee: 1, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + requiredMiningFee, err := CalculateMiningFeesRequired(tc.size, tc.fees) + require.NoError(t, err) + require.Equal(t, tc.expectedRequiredMiningFee, requiredMiningFee) + }) + } +} + +func Test_checkTxSize(t *testing.T) { + type args struct { + txSize int + policy *bitcoin.Settings + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid tx size", + args: args{ + txSize: 100, + policy: &bitcoin.Settings{ + MaxTxSizePolicy: 10000000, + }, + }, + wantErr: false, + }, + { + name: "invalid tx size", + args: args{ + txSize: maxBlockSize + 1, + policy: &bitcoin.Settings{ + MaxTxSizePolicy: 10000000, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := checkTxSize(tt.args.txSize, tt.args.policy); (err != nil) != tt.wantErr { + t.Errorf("checkTxSize() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +//nolint:funlen - don't need to check length of test functions +func Test_checkOutputs(t *testing.T) { + type args struct { + tx *bt.Tx + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid output", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{{Satoshis: 100, LockingScript: validLockingScript}}, + }, + }, + wantErr: false, + }, + { + name: "invalid satoshis > max", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{{Satoshis: maxSatoshis + 1, LockingScript: validLockingScript}}, + }, + }, + wantErr: true, + }, + { + name: "invalid satoshis == 0", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{{Satoshis: 0, LockingScript: validLockingScript}}, + }, + }, + wantErr: true, + }, + { + name: "valid satoshis == 0, op return", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{{Satoshis: 0, LockingScript: opReturnLockingScript}}, + }, + }, + wantErr: false, + }, + { + name: "invalid satoshis, op return", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{{Satoshis: 100, LockingScript: opReturnLockingScript}}, + }, + }, + wantErr: true, + }, + { + name: "invalid total satoshis", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{ + {Satoshis: maxSatoshis - 100, LockingScript: validLockingScript}, + {Satoshis: maxSatoshis - 100, LockingScript: validLockingScript}, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := checkOutputs(tt.args.tx); (err != nil) != tt.wantErr { + t.Errorf("checkOutputs() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_checkInputs(t *testing.T) { + type args struct { + tx *bt.Tx + } + + coinbaseInput := &bt.Input{} + coinbaseInput.PreviousTxSatoshis = 100 + _ = coinbaseInput.PreviousTxIDAddStr(coinbaseTxID) + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "invalid coinbase input", + args: args{ + tx: &bt.Tx{Inputs: []*bt.Input{coinbaseInput}}, + }, + wantErr: true, + }, + { + name: "valid input", + args: args{ + tx: &bt.Tx{Inputs: []*bt.Input{{PreviousTxSatoshis: 100}}}, + }, + wantErr: false, + }, + { + name: "invalid input satoshis", + args: args{ + tx: &bt.Tx{Inputs: []*bt.Input{{PreviousTxSatoshis: maxSatoshis + 1}}}, + }, + wantErr: true, + }, + { + name: "invalid input satoshis", + args: args{ + tx: &bt.Tx{ + Inputs: []*bt.Input{{ + PreviousTxSatoshis: maxSatoshis - 100, + }, { + PreviousTxSatoshis: maxSatoshis - 100, + }}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := checkInputs(tt.args.tx); (err != nil) != tt.wantErr { + t.Errorf("checkInputs() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_sigOpsCheck(t *testing.T) { + type args struct { + tx *bt.Tx + policy *bitcoin.Settings + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid sigops", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{{LockingScript: validLockingScript}}, + }, + policy: &bitcoin.Settings{ + MaxTxSigopsCountsPolicy: 4294967295, + }, + }, + wantErr: false, + }, + { + name: "invalid sigops - too many sigops", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{ + { + LockingScript: validLockingScript, + }, + { + LockingScript: validLockingScript, + }, + }, + }, + policy: &bitcoin.Settings{ + MaxTxSigopsCountsPolicy: 1, + }, + }, + wantErr: true, + }, + { + name: "valid sigops - default policy", + args: args{ + tx: &bt.Tx{ + Outputs: []*bt.Output{ + { + LockingScript: validLockingScript, + }, + { + LockingScript: validLockingScript, + }, + }, + }, + policy: &bitcoin.Settings{ + MaxTxSigopsCountsPolicy: 4294967295, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := sigOpsCheck(tt.args.tx, tt.args.policy); (err != nil) != tt.wantErr { + t.Errorf("sigOpsCheck() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_pushDataCheck(t *testing.T) { + type args struct { + tx *bt.Tx + } + + validUnlockingBytes, _ := hex.DecodeString("4730440220318d23e6fd7dd5ace6e8dc1888b363a053552f48ecc166403a1cc65db5e16aca02203a9ad254cb262f50c89487ffd72e8ddd8536c07f4b230d13a2ccd1435898e89b412102dd7dce95e52345704bbb4df4e4cfed1f8eaabf8260d33597670e3d232c491089") + validUnlockingScript := bscript.Script(validUnlockingBytes) + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid push data", + args: args{ + tx: &bt.Tx{ + Inputs: []*bt.Input{{ + UnlockingScript: &validUnlockingScript, + }}, + }, + }, + wantErr: false, + }, + { + name: "invalid push data", + args: args{ + tx: &bt.Tx{ + Inputs: []*bt.Input{{ + UnlockingScript: validLockingScript, + }}, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := pushDataCheck(tt.args.tx); (err != nil) != tt.wantErr { + t.Errorf("pushDataCheck() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 04e18c52d..306bf5888 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -1,6 +1,8 @@ package validator import ( + "context" + "github.com/bitcoin-sv/arc/internal/beef" "github.com/libsv/go-bt/v2" ) @@ -15,10 +17,57 @@ type OutpointData struct { Satoshis int64 } -type Validator interface { - // ValidateEFTransaction Please note that bt.Tx should have all the fields of each input populated. - ValidateEFTransaction(tx *bt.Tx, skipFeeValidation bool, skipScriptValidation bool) error - ValidateBeef(beef *beef.BEEF, skipFeeValidation bool, skipScriptValidation bool) (*bt.Tx, error) +type FeeValidation byte + +const ( + NoneFeeValidation FeeValidation = iota + StandardFeeValidation +) + +type ScriptValidation byte + +const ( + NoneScriptValidation ScriptValidation = iota + StandardScriptValidation +) + +type DefaultValidator interface { + ValidateTransaction(ctx context.Context, tx *bt.Tx, feeValidation FeeValidation, scriptValidation ScriptValidation) error IsExtended(tx *bt.Tx) bool - IsBeef(txHex []byte) bool +} + +type BeefValidator interface { + ValidateTransaction(ctx context.Context, beef *beef.BEEF, feeValidation FeeValidation, scriptValidation ScriptValidation) (*bt.Tx, error) +} + +type HexFormat byte + +const ( + Raw HexFormat = iota + Ef + Beef +) + +func GetHexFormat(hex []byte) HexFormat { + if beef.CheckBeefFormat(hex) { + return Beef + } + + if isEf(hex) { + return Ef + } + + return Raw +} + +func isEf(hex []byte) bool { + // check markers - first 10 bytes + // 4 bytes for version + 6 bytes for the marker - 0000000000EF + return len(hex) > 10 && + hex[4] == 0 && + hex[5] == 0 && + hex[6] == 0 && + hex[7] == 0 && + hex[8] == 0 && + hex[9] == 0xEF } diff --git a/pkg/api/handler/default.go b/pkg/api/handler/default.go index 5020ce238..e941e2803 100644 --- a/pkg/api/handler/default.go +++ b/pkg/api/handler/default.go @@ -13,6 +13,7 @@ import ( "github.com/bitcoin-sv/arc/config" "github.com/bitcoin-sv/arc/internal/beef" "github.com/bitcoin-sv/arc/internal/validator" + beefValidator "github.com/bitcoin-sv/arc/internal/validator/beef" defaultValidator "github.com/bitcoin-sv/arc/internal/validator/default" "github.com/bitcoin-sv/arc/internal/version" "github.com/bitcoin-sv/arc/pkg/api" @@ -149,7 +150,8 @@ func (m ArcDefaultHandler) POSTTransaction(ctx echo.Context, params api.POSTTran var response *api.TransactionResponse var responseErr *api.ErrorFields - if beef.CheckBeefFormat(transactionHex) { + hexFormat := validator.GetHexFormat(transactionHex) + if hexFormat == validator.Beef { transaction, response, responseErr = m.processBEEFTransaction(ctx.Request().Context(), transactionHex, transactionOptions) } else { transaction, response, responseErr = m.processEFTransaction(ctx.Request().Context(), transactionHex, transactionOptions) @@ -294,14 +296,14 @@ func getTransactionsOptions(params api.POSTTransactionsParams, rejectedCallbackU } func (m ArcDefaultHandler) processEFTransaction(ctx context.Context, transactionHex []byte, transactionOptions *metamorph.TransactionOptions) (*bt.Tx, *api.TransactionResponse, *api.ErrorFields) { - txValidator := defaultValidator.New(m.NodePolicy) transaction, err := bt.NewTxFromBytes(transactionHex) if err != nil { return nil, nil, api.NewErrorFields(api.ErrStatusBadRequest, err.Error()) } - if arcError := m.validateEFTransaction(ctx, txValidator, transaction, transactionOptions); arcError != nil { + v := defaultValidator.New(m.NodePolicy) + if arcError := m.validateEFTransaction(ctx, v, transaction, transactionOptions); arcError != nil { return nil, nil, arcError } @@ -330,7 +332,6 @@ func (m ArcDefaultHandler) processEFTransaction(ctx context.Context, transaction } func (m ArcDefaultHandler) processBEEFTransaction(ctx context.Context, transactionHex []byte, transactionOptions *metamorph.TransactionOptions) (*bt.Tx, *api.TransactionResponse, *api.ErrorFields) { - txValidator := defaultValidator.New(m.NodePolicy) beefTx, _, err := beef.DecodeBEEF(transactionHex) if err != nil { @@ -338,7 +339,8 @@ func (m ArcDefaultHandler) processBEEFTransaction(ctx context.Context, transacti return nil, nil, api.NewErrorFields(api.ErrStatusMalformed, errStr) } - if err := m.validateBEEFTransaction(ctx, txValidator, beefTx, transactionOptions); err != nil { + v := beefValidator.New(m.NodePolicy) + if err := m.validateBEEFTransaction(ctx, v, beefTx, transactionOptions); err != nil { return nil, nil, err } @@ -388,12 +390,10 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, transactions txIds := make([]string, 0) txErrors := make([]interface{}, 0) - txValidator := defaultValidator.New(m.NodePolicy) - for len(transactionsHexes) != 0 { - isBeefFormat := txValidator.IsBeef(transactionsHexes) + hexFormat := validator.GetHexFormat(transactionsHexes) - if isBeefFormat { + if hexFormat == validator.Beef { beefTx, remainingBytes, err := beef.DecodeBEEF(transactionsHexes) if err != nil { errStr := fmt.Sprintf("error decoding BEEF: %s", err.Error()) @@ -402,7 +402,10 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, transactions transactionsHexes = remainingBytes - if errTx, err := txValidator.ValidateBeef(beefTx, transactionOptions.SkipFeeValidation, transactionOptions.SkipScriptValidation); err != nil { + feeOpts, scriptOpts := toValidationOpts(transactionOptions) + v := beefValidator.New(m.NodePolicy) + + if errTx, err := v.ValidateTransaction(ctx, beefTx, feeOpts, scriptOpts); err != nil { _, arcError := m.handleError(ctx, errTx, err) txErrors = append(txErrors, arcError) continue @@ -422,8 +425,8 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, transactions } transactionsHexes = transactionsHexes[bytesUsed:] - - if arcError := m.validateEFTransaction(ctx, txValidator, transaction, transactionOptions); arcError != nil { + v := defaultValidator.New(m.NodePolicy) + if arcError := m.validateEFTransaction(ctx, v, transaction, transactionOptions); arcError != nil { txErrors = append(txErrors, arcError) continue } @@ -472,9 +475,10 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, transactions return transactions, transactionsOutputs, nil } -func (m ArcDefaultHandler) validateEFTransaction(ctx context.Context, txValidator validator.Validator, transaction *bt.Tx, transactionOptions *metamorph.TransactionOptions) *api.ErrorFields { +func (m ArcDefaultHandler) validateEFTransaction(ctx context.Context, txValidator validator.DefaultValidator, transaction *bt.Tx, transactionOptions *metamorph.TransactionOptions) *api.ErrorFields { // the validator expects an extended transaction // we must enrich the transaction with the missing data + // TODO: move morphing to validator if !txValidator.IsExtended(transaction) { err := m.extendTransaction(ctx, transaction) if err != nil { @@ -485,7 +489,9 @@ func (m ArcDefaultHandler) validateEFTransaction(ctx context.Context, txValidato } if !transactionOptions.SkipTxValidation { - if err := txValidator.ValidateEFTransaction(transaction, transactionOptions.SkipFeeValidation, transactionOptions.SkipScriptValidation); err != nil { + feeOpts, scriptOpts := toValidationOpts(transactionOptions) + + if err := txValidator.ValidateTransaction(ctx, transaction, feeOpts, scriptOpts); err != nil { statusCode, arcError := m.handleError(ctx, transaction, err) m.logger.Error("failed to validate transaction", slog.String("id", transaction.TxID()), slog.Int("id", int(statusCode)), slog.String("err", err.Error())) return arcError @@ -528,8 +534,10 @@ func (m ArcDefaultHandler) extendTransaction(ctx context.Context, transaction *b return nil } -func (m ArcDefaultHandler) validateBEEFTransaction(ctx context.Context, txValidator validator.Validator, beefTx *beef.BEEF, transactionOptions *metamorph.TransactionOptions) *api.ErrorFields { - if errTx, err := txValidator.ValidateBeef(beefTx, transactionOptions.SkipFeeValidation, transactionOptions.SkipTxValidation); err != nil { +func (m ArcDefaultHandler) validateBEEFTransaction(ctx context.Context, txValidator validator.BeefValidator, beefTx *beef.BEEF, transactionOptions *metamorph.TransactionOptions) *api.ErrorFields { + feeOpts, scriptOpts := toValidationOpts(transactionOptions) + + if errTx, err := txValidator.ValidateTransaction(ctx, beefTx, feeOpts, scriptOpts); err != nil { _, arcError := m.handleError(ctx, errTx, err) return arcError } @@ -657,3 +665,17 @@ const ( func PtrTo[T any](v T) *T { return &v } + +func toValidationOpts(opts *metamorph.TransactionOptions) (validator.FeeValidation, validator.ScriptValidation) { + fv := validator.StandardFeeValidation + if opts.SkipFeeValidation { + fv = validator.NoneFeeValidation + } + + sv := validator.StandardScriptValidation + if opts.SkipScriptValidation { + sv = validator.NoneScriptValidation + } + + return fv, sv +} From 60a3c1b587647645aa8e601a05fc0fd8457bb481 Mon Sep 17 00:00:00 2001 From: Arkadiusz Osowski Date: Tue, 9 Jul 2024 12:08:41 +0200 Subject: [PATCH 2/2] ARCO-155: adjust to review - change names, remove comments --- .../{validation.go => common_validation.go} | 28 +------------------ ...tion_test.go => common_validation_test.go} | 0 .../validator/default/default_validator.go | 3 +- internal/validator/validator.go | 12 ++++---- pkg/api/handler/default.go | 4 +-- 5 files changed, 10 insertions(+), 37 deletions(-) rename internal/validator/{validation.go => common_validation.go} (84%) rename internal/validator/{validation_test.go => common_validation_test.go} (100%) diff --git a/internal/validator/validation.go b/internal/validator/common_validation.go similarity index 84% rename from internal/validator/validation.go rename to internal/validator/common_validation.go index 2ebbbb7db..d71a2a749 100644 --- a/internal/validator/validation.go +++ b/internal/validator/common_validation.go @@ -93,27 +93,6 @@ func CommonValidateTransaction(policy *bitcoin.Settings, tx *bt.Tx) error { return NewError(err, api.ErrStatusMalformed) } - // // 10) Reject if the sum of input values is less than sum of output values - // // 11) Reject if transaction fee would be too low (minRelayTxFee) to get into an empty block. - // switch feeValidation { - // case StandardFeeValidation: - // if err := checkFees(tx, api.FeesToBtFeeQuote(policy.MinMiningTxFee)); err != nil { - // return NewError(err, api.ErrStatusFees) - // } - // case DeepFeeValidation: - // if err := v.deepCheckFees(ctx, tx, api.FeesToBtFeeQuote(policy.MinMiningTxFee)); err != nil { - // return NewError(err, api.ErrStatusFees) // TODO: add new status - // } - // default: - // } - - // // 12) The unlocking scripts for each input must validate against the corresponding output locking scripts - // if scriptValidation == StandardScriptValidation { - // if err := checkScripts(tx); err != nil { - // return NewError(err, api.ErrStatusUnlockingScripts) - // } - // } - // everything checks out return nil } @@ -157,12 +136,7 @@ func checkInputs(tx *bt.Tx) error { if hex.EncodeToString(input.PreviousTxID()) == coinbaseTxID { return NewError(fmt.Errorf("transaction input %d is a coinbase input", index), api.ErrStatusInputs) } - /* lots of our valid test transactions have this sequence number, is this not allowed? - if input.SequenceNumber == 0xffffffff { - fmt.Printf("input %d has sequence number 0xffffffff, txid = %s", index, tx.TxID()) - return validator.NewError(fmt.Errorf("transaction input %d sequence number is invalid", index), arc.ErrStatusInputs) - } - */ + if input.PreviousTxSatoshis > maxSatoshis { return NewError(fmt.Errorf("transaction input %d satoshis is too high", index), api.ErrStatusInputs) } diff --git a/internal/validator/validation_test.go b/internal/validator/common_validation_test.go similarity index 100% rename from internal/validator/validation_test.go rename to internal/validator/common_validation_test.go diff --git a/internal/validator/default/default_validator.go b/internal/validator/default/default_validator.go index 804166bc6..8189a34a6 100644 --- a/internal/validator/default/default_validator.go +++ b/internal/validator/default/default_validator.go @@ -42,8 +42,7 @@ func (v *DefaultValidator) ValidateTransaction(ctx context.Context, tx *bt.Tx, f return err } case validator.NoneFeeValidation: - fallthrough - default: + // Do not handle the default case on purpose; we shouldn't assume that other types of validation should be omitted } // 12) The unlocking scripts for each input must validate against the corresponding output locking scripts diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 306bf5888..9f5e78947 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -43,21 +43,21 @@ type BeefValidator interface { type HexFormat byte const ( - Raw HexFormat = iota - Ef - Beef + RawHex HexFormat = iota + EfHex + BeefHex ) func GetHexFormat(hex []byte) HexFormat { if beef.CheckBeefFormat(hex) { - return Beef + return BeefHex } if isEf(hex) { - return Ef + return EfHex } - return Raw + return RawHex } func isEf(hex []byte) bool { diff --git a/pkg/api/handler/default.go b/pkg/api/handler/default.go index e941e2803..4612827ca 100644 --- a/pkg/api/handler/default.go +++ b/pkg/api/handler/default.go @@ -151,7 +151,7 @@ func (m ArcDefaultHandler) POSTTransaction(ctx echo.Context, params api.POSTTran var responseErr *api.ErrorFields hexFormat := validator.GetHexFormat(transactionHex) - if hexFormat == validator.Beef { + if hexFormat == validator.BeefHex { transaction, response, responseErr = m.processBEEFTransaction(ctx.Request().Context(), transactionHex, transactionOptions) } else { transaction, response, responseErr = m.processEFTransaction(ctx.Request().Context(), transactionHex, transactionOptions) @@ -393,7 +393,7 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, transactions for len(transactionsHexes) != 0 { hexFormat := validator.GetHexFormat(transactionsHexes) - if hexFormat == validator.Beef { + if hexFormat == validator.BeefHex { beefTx, remainingBytes, err := beef.DecodeBEEF(transactionsHexes) if err != nil { errStr := fmt.Sprintf("error decoding BEEF: %s", err.Error())