diff --git a/docker-compose.regtest.yml b/docker-compose.regtest.yml index 5a070ca4c..de73c37f8 100644 --- a/docker-compose.regtest.yml +++ b/docker-compose.regtest.yml @@ -26,7 +26,7 @@ services: ports: - "7070:7070" environment: - - ARKD_LOG_LEVEL=5 + - ARKD_LOG_LEVEL=6 - ARKD_NO_MACAROONS=true - ARKD_VTXO_TREE_EXPIRY=20 - ARKD_SCHEDULER_TYPE=block diff --git a/go.mod b/go.mod index 5e4f951e1..1590c2b36 100644 --- a/go.mod +++ b/go.mod @@ -16,12 +16,13 @@ replace github.com/arkade-os/arkd/pkg/kvdb => ./pkg/kvdb require ( github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0 + github.com/ark-network/ark/common v0.0.0-20250606113434-241d3e1ec7cb github.com/arkade-os/arkd/api-spec v0.0.0-20250708133905-efe11fcfce2d - github.com/arkade-os/arkd/pkg/ark-lib v0.0.0-20250708133905-efe11fcfce2d + github.com/arkade-os/arkd/pkg/ark-lib v0.7.1-0.20250811122941-696108b42f69 github.com/arkade-os/arkd/pkg/arkd-wallet v0.0.0-00010101000000-000000000000 github.com/arkade-os/arkd/pkg/kvdb v0.0.0-20250606113434-241d3e1ec7cb github.com/arkade-os/arkd/pkg/macaroons v0.0.0-00010101000000-000000000000 - github.com/arkade-os/go-sdk v0.6.3-0.20250708135236-08d1624ee483 + github.com/arkade-os/go-sdk v0.6.3-0.20250811123942-2481769116c7 github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcwallet/walletdb v1.4.2 github.com/dgraph-io/badger/v4 v4.3.0 @@ -47,7 +48,7 @@ require ( go.opentelemetry.io/otel/metric v1.34.0 go.opentelemetry.io/otel/sdk v1.34.0 go.opentelemetry.io/otel/sdk/metric v1.34.0 - google.golang.org/grpc v1.69.4 + google.golang.org/grpc v1.71.0 google.golang.org/protobuf v1.36.6 gopkg.in/macaroon-bakery.v2 v2.3.0 gopkg.in/macaroon.v2 v2.1.0 @@ -205,18 +206,18 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/sync v0.12.0 // indirect - golang.org/x/term v0.29.0 // indirect + golang.org/x/term v0.30.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.29.0 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect modernc.org/libc v1.59.3 // indirect - modernc.org/mathutil v1.6.0 // indirect + modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect @@ -259,12 +260,12 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.35.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect - golang.org/x/net v0.36.0 - golang.org/x/sys v0.30.0 // indirect + golang.org/x/net v0.37.0 + golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 58d9c327d..ea37dc196 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,10 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSi github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/arkade-os/go-sdk v0.6.3-0.20250708135236-08d1624ee483 h1:/oa4RfDVJASvgaYFfzfPEUQTTJ8e/cs3wfPrTS+rk04= -github.com/arkade-os/go-sdk v0.6.3-0.20250708135236-08d1624ee483/go.mod h1:Lu2hpbFIGuPL01E7njIyqm1FtysJaEvgC4z1k0fznP8= +github.com/ark-network/ark/common v0.0.0-20250606113434-241d3e1ec7cb h1:TvNEA7OJF3CZU5A7wdXOHBTgrIDU4KZ+IBxi6+WEC1g= +github.com/ark-network/ark/common v0.0.0-20250606113434-241d3e1ec7cb/go.mod h1:A8c6gJaMt6wTDkZCPY8UpQmFkHBpBwg+zb1RD/wbRq4= +github.com/arkade-os/go-sdk v0.6.3-0.20250811123942-2481769116c7 h1:6Tnb2msL8XY3daGz86uex3JHNZrJFiE6+KMCh2JK3KM= +github.com/arkade-os/go-sdk v0.6.3-0.20250811123942-2481769116c7/go.mod h1:bAJyRplt/GJD3hcomHlL5Zdiop2ubrcdCY1gzytRKeA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= @@ -735,8 +737,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= @@ -777,8 +779,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -827,16 +829,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -886,10 +888,10 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM= google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc= -google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= -google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -898,8 +900,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -973,8 +975,8 @@ modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbP modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/libc v1.59.3 h1:A4QAp1lRSn2/b4aU+wBtq+yeKgq/2BUevrj0p1ZNy2M= modernc.org/libc v1.59.3/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= diff --git a/internal/core/application/service.go b/internal/core/application/service.go index c98c946cd..94d8ba568 100644 --- a/internal/core/application/service.go +++ b/internal/core/application/service.go @@ -448,9 +448,11 @@ func (s *service) SubmitOffchainTx( return nil, "", "", fmt.Errorf("failed to parse vtxo script: %s", err) } + arkadeScript := txutils.GetArkadeScript(input) + // validate the vtxo script if err := vtxoScript.Validate( - s.signerPubkey, s.unilateralExitDelay, s.allowCSVBlockType, + s.signerPubkey, s.unilateralExitDelay, s.allowCSVBlockType, arkadeScript, ); err != nil { return nil, "", "", fmt.Errorf("invalid vtxo script: %s", err) } @@ -565,6 +567,7 @@ func (s *service) SubmitOffchainTx( CheckpointTapscript: checkpointTapscript, RevealedTapscripts: tapscripts, Amount: int64(vtxo.Amount), + ArkadeScript: arkadeScript, }) } @@ -688,7 +691,8 @@ func (s *service) SubmitOffchainTx( } // verify the tapscript signatures - if valid, _, err := s.builder.VerifyTapscriptPartialSigs(signedArkTx); err != nil || !valid { + if valid, _, err := s.builder.VerifyTapscriptPartialSigs(signedArkTx, true); err != nil || + !valid { return nil, "", "", fmt.Errorf("invalid ark tx signature(s): %s", err) } @@ -762,7 +766,8 @@ func (s *service) FinalizeOffchainTx( finalCheckpointTxsMap := make(map[string]string) for _, checkpoint := range finalCheckpointTxs { // verify the tapscript signatures - valid, checkpointTxid, err := s.builder.VerifyTapscriptPartialSigs(checkpoint) + // we do not execute the arkade script in checkpoint txs cause we assume the covenant applies to the ark tx + valid, checkpointTxid, err := s.builder.VerifyTapscriptPartialSigs(checkpoint, false) if err != nil || !valid { return fmt.Errorf("invalid tx signature: %s", err) } @@ -2235,7 +2240,7 @@ func (s *service) validateBoardingInput( if err := vtxoScript.Validate(s.signerPubkey, arklib.RelativeLocktime{ Type: s.boardingExitDelay.Type, Value: s.boardingExitDelay.Value, - }, s.allowCSVBlockType); err != nil { + }, s.allowCSVBlockType, nil); err != nil { return nil, fmt.Errorf("invalid vtxo script: %s", err) } @@ -2287,8 +2292,10 @@ func (s *service) validateVtxoInput( } // validate the vtxo script + // arkade script always nil for now + // TODO: allow arkade script to be passed in intent registration if err := vtxoScript.Validate( - s.signerPubkey, s.unilateralExitDelay, s.allowCSVBlockType, + s.signerPubkey, s.unilateralExitDelay, s.allowCSVBlockType, nil, ); err != nil { return fmt.Errorf("invalid vtxo script: %s", err) } @@ -2336,7 +2343,7 @@ func (s *service) verifyForfeitTxsSigs(txs []string) error { defer wg.Done() for tx := range jobs { - valid, txid, err := s.builder.VerifyTapscriptPartialSigs(tx) + valid, txid, err := s.builder.VerifyTapscriptPartialSigs(tx, false) if err != nil { errChan <- fmt.Errorf("failed to validate forfeit tx %s: %s", txid, err) return diff --git a/internal/core/application/utils.go b/internal/core/application/utils.go index 1e18a5d76..65973f043 100644 --- a/internal/core/application/utils.go +++ b/internal/core/application/utils.go @@ -173,7 +173,7 @@ func newBoardingInput( } if err := boardingScript.Validate( - signerPubkey, boardingExitDelay, blockTypeCSVAllowed, + signerPubkey, boardingExitDelay, blockTypeCSVAllowed, nil, ); err != nil { return nil, err } diff --git a/internal/core/ports/tx_builder.go b/internal/core/ports/tx_builder.go index 2e3857e8e..d97dc4571 100644 --- a/internal/core/ports/tx_builder.go +++ b/internal/core/ports/tx_builder.go @@ -55,7 +55,10 @@ type TxBuilder interface { vtxoTreeExpiry *arklib.RelativeLocktime, batchOutputs SweepableBatchOutput, err error, ) FinalizeAndExtract(tx string) (txhex string, err error) - VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error) + VerifyTapscriptPartialSigs( + tx string, + withArkadeScriptInterpreter bool, + ) (valid bool, txid string, err error) VerifyAndCombinePartialTx(dest string, src string) (string, error) CountSignedTaprootInputs(tx string) (int, error) } diff --git a/internal/infrastructure/live-store/live_store_test.go b/internal/infrastructure/live-store/live_store_test.go index ab2084ac7..1087006fd 100644 --- a/internal/infrastructure/live-store/live_store_test.go +++ b/internal/infrastructure/live-store/live_store_test.go @@ -468,8 +468,9 @@ func (m *mockedTxBuilder) FinalizeAndExtract(tx string) (txhex string, err error func (m *mockedTxBuilder) VerifyTapscriptPartialSigs( tx string, + withArkadeScriptInterpreter bool, ) (valid bool, txid string, err error) { - args := m.Called(tx) + args := m.Called(tx, withArkadeScriptInterpreter) res0 := args.Get(0).(bool) res1 := args.Get(1).(string) return res0, res1, args.Error(2) diff --git a/internal/infrastructure/tx-builder/covenantless/builder.go b/internal/infrastructure/tx-builder/covenantless/builder.go index 25286c3ee..eb822778f 100644 --- a/internal/infrastructure/tx-builder/covenantless/builder.go +++ b/internal/infrastructure/tx-builder/covenantless/builder.go @@ -21,6 +21,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + log "github.com/sirupsen/logrus" ) type txBuilder struct { @@ -46,16 +47,22 @@ func (b *txBuilder) GetTxid(tx string) (string, error) { return ptx.UnsignedTx.TxID(), nil } -func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) { +func (b *txBuilder) VerifyTapscriptPartialSigs( + tx string, + withArkadeScriptInterpreter bool, +) (bool, string, error) { ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true) if err != nil { return false, "", err } - return b.verifyTapscriptPartialSigs(ptx) + return b.verifyTapscriptPartialSigs(ptx, withArkadeScriptInterpreter) } -func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, string, error) { +func (b *txBuilder) verifyTapscriptPartialSigs( + ptx *psbt.Packet, + withArkadeScriptInterpreter bool, +) (bool, string, error) { txid := ptx.UnsignedTx.TxID() operatorPubkey, err := b.wallet.GetPubkey(context.Background()) @@ -115,7 +122,48 @@ func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, string, } } - // we don't need to check if operator signed + // execute ark script if present in psbt input + arkScript := txutils.GetArkadeScript(input) + if len(arkScript) > 0 { + arkadeScriptHash := script.ArkadeScriptHash(arkScript) + arkadeScriptKey := script.ComputeArkadeScriptPublicKey(operatorPubkey, arkadeScriptHash) + + // we don't need to check if server tweaked key signed + keys[hex.EncodeToString(schnorr.SerializePubKey(arkadeScriptKey))] = true + + if withArkadeScriptInterpreter { + witness, err := txutils.GetArkadeScriptWitness(input) + if err != nil { + return false, txid, err + } + prevouts := make(map[wire.OutPoint]*wire.TxOut) + for i, input := range ptx.Inputs { + if input.WitnessUtxo == nil { + return false, txid, fmt.Errorf( + "missing prevout for input %d, cannot validate ark script", + i, + ) + } + + outpoint := ptx.UnsignedTx.TxIn[i].PreviousOutPoint + prevouts[outpoint] = input.WitnessUtxo + } + + prevoutFetcher := txscript.NewMultiPrevOutFetcher(prevouts) + + if err := script.ExecuteArkadeScript( + arkScript, + ptx.UnsignedTx, + prevoutFetcher, + index, + witness, + ); err != nil { + return false, txid, fmt.Errorf("arkade script execution failed") + } + } + } + + // we don't need to check if server signed keys[hex.EncodeToString(schnorr.SerializePubKey(operatorPubkey))] = true if len(tapLeaf.ControlBlock) == 0 { @@ -157,21 +205,45 @@ func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, string, } if !sig.Verify(preimage, pubkey) { - return false, txid, nil + if log.IsLevelEnabled(log.TraceLevel) { + b64Tx, err := ptx.B64Encode() + if err != nil { + return false, txid, err + } + + log.WithFields(log.Fields{ + "txid": txid, + "index": index, + "signature": hex.EncodeToString(tapScriptSig.Signature), + "pubkey": hex.EncodeToString(schnorr.SerializePubKey(pubkey)), + "preimage": hex.EncodeToString(preimage), + "tx": b64Tx, + }).Trace("invalid signature") + } + + return false, txid, fmt.Errorf( + "invalid signature for input %d, %s", + index, + hex.EncodeToString(tapScriptSig.Signature), + ) } keys[hex.EncodeToString(schnorr.SerializePubKey(pubkey))] = true } - missingSigs := 0 + missingSigs := make([]string, 0) for key := range keys { if !keys[key] { - missingSigs++ + missingSigs = append(missingSigs, key) } } - if missingSigs > 0 { - return false, txid, fmt.Errorf("missing %d signatures", missingSigs) + if len(missingSigs) > 0 { + return false, txid, fmt.Errorf( + "missing %d signatures: %s", + len(missingSigs), + strings.Join(missingSigs, ", "), + ) } } @@ -192,20 +264,22 @@ func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) { return "", err } - conditionWitness, err := txutils.GetConditionWitness(in) - if err != nil { - return "", err - } + args := make(map[string]any) - args := make(map[string][]byte) - if len(conditionWitness) > 0 { - var conditionWitnessBytes bytes.Buffer - if err := psbt.WriteTxWitness( - &conditionWitnessBytes, conditionWitness, - ); err != nil { + switch closure.(type) { + case *script.ConditionMultisigClosure: + conditionWitness, err := txutils.GetConditionWitness(in) + if err != nil { return "", err } - args[string(txutils.CONDITION_WITNESS_KEY_PREFIX)] = conditionWitnessBytes.Bytes() + + if len(conditionWitness) > 0 { + var conditionWitnessBytes bytes.Buffer + if err := psbt.WriteTxWitness(&conditionWitnessBytes, conditionWitness); err != nil { + return "", err + } + args[script.ConditionWitnessKey] = conditionWitnessBytes.Bytes() + } } for _, sig := range in.TaprootScriptSpendSig { diff --git a/pkg/ark-cli/go.mod b/pkg/ark-cli/go.mod index 2cbd4e842..5f88c58f6 100644 --- a/pkg/ark-cli/go.mod +++ b/pkg/ark-cli/go.mod @@ -7,8 +7,8 @@ replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v replace github.com/arkade-os/arkd/pkg/ark-lib => ../ark-lib require ( - github.com/arkade-os/arkd/pkg/ark-lib v0.0.0-20250708155328-721172a83dba - github.com/arkade-os/go-sdk v0.6.3-0.20250708222801-e64fe0de9c61 + github.com/arkade-os/arkd/pkg/ark-lib v0.7.1-0.20250811122941-696108b42f69 + github.com/arkade-os/go-sdk v0.6.3-0.20250811123942-2481769116c7 github.com/urfave/cli/v2 v2.27.4 golang.org/x/term v0.30.0 ) @@ -113,7 +113,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect modernc.org/libc v1.59.3 // indirect - modernc.org/mathutil v1.6.0 // indirect + modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.33.1 // indirect modernc.org/strutil v1.2.0 // indirect diff --git a/pkg/ark-cli/go.sum b/pkg/ark-cli/go.sum index a3a6c47dd..a126d33aa 100644 --- a/pkg/ark-cli/go.sum +++ b/pkg/ark-cli/go.sum @@ -16,8 +16,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/arkade-os/go-sdk v0.6.3-0.20250708222801-e64fe0de9c61 h1:NZIj9Hb6cV0JCs316crPTMf28VQDipYyySsMWnBiqog= -github.com/arkade-os/go-sdk v0.6.3-0.20250708222801-e64fe0de9c61/go.mod h1:HM/f32H6LSlxlT832PnAqQmVT85J1uww6DetUXuMdyY= +github.com/arkade-os/go-sdk v0.6.3-0.20250811123942-2481769116c7 h1:6Tnb2msL8XY3daGz86uex3JHNZrJFiE6+KMCh2JK3KM= +github.com/arkade-os/go-sdk v0.6.3-0.20250811123942-2481769116c7/go.mod h1:bAJyRplt/GJD3hcomHlL5Zdiop2ubrcdCY1gzytRKeA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= @@ -666,8 +666,8 @@ modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbP modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/libc v1.59.3 h1:A4QAp1lRSn2/b4aU+wBtq+yeKgq/2BUevrj0p1ZNy2M= modernc.org/libc v1.59.3/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= diff --git a/pkg/ark-lib/arkade/engine.go b/pkg/ark-lib/arkade/engine.go new file mode 100644 index 000000000..447fa4ad5 --- /dev/null +++ b/pkg/ark-lib/arkade/engine.go @@ -0,0 +1,1464 @@ +package arkade + +import ( + "bytes" + "fmt" + "math" + "math/big" + "strings" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +const ( + // payToTaprootDataSize is the size of the witness program push for + // taproot spends. This will be the serialized x-coordinate of the + // top-level taproot output public key. + payToTaprootDataSize = 32 +) + +const ( + // blankCodeSepValue is the value of the code separator position in the + // tapscript sighash when no code separator was found in the script. + blankCodeSepValue = math.MaxUint32 +) + +// halforder is used to tame ECDSA malleability (see BIP0062). +var halfOrder = new(big.Int).Rsh(btcec.S256().N, 1) + +// taprootExecutionCtx houses the special context-specific information we need +// to validate a taproot script spend. This includes the annex, the running sig +// op count tally, and other relevant information. +type taprootExecutionCtx struct { + annex []byte + + codeSepPos uint32 + + tapLeafHash chainhash.Hash + + sigOpsBudget int32 + + mustSucceed bool +} + +// sigOpsDelta is both the starting budget for sig ops for tapscript +// verification, as well as the decrease in the total budget when we encounter +// a signature. +const sigOpsDelta = 50 + +// tallysigOp attempts to decrease the current sig ops budget by sigOpsDelta. +// An error is returned if after subtracting the delta, the budget is below +// zero. +func (t *taprootExecutionCtx) tallysigOp() error { + t.sigOpsBudget -= sigOpsDelta + + if t.sigOpsBudget < 0 { + return scriptError(txscript.ErrTaprootMaxSigOps, "") + } + + return nil +} + +// newTaprootExecutionCtx returns a fresh instance of the taproot execution +// context. +func newTaprootExecutionCtx(inputWitnessSize int32) *taprootExecutionCtx { + return &taprootExecutionCtx{ + codeSepPos: blankCodeSepValue, + sigOpsBudget: sigOpsDelta + inputWitnessSize, + } +} + +// Engine is the virtual machine that executes scripts. +type Engine struct { + // The following fields are set when the engine is created and must not be + // changed afterwards. The entries of the signature cache are mutated + // during execution, however, the cache pointer itself is not changed. + // + // flags specifies the additional flags which modify the execution behavior + // of the engine. + // + // tx identifies the transaction that contains the input which in turn + // contains the signature script being executed. + // + // txIdx identifies the input index within the transaction that contains + // the signature script being executed. + // + // version specifies the version of the public key script to execute. Since + // signature scripts redeem public keys scripts, this means the same version + // also extends to signature scripts and redeem scripts in the case of + // pay-to-script-hash. + // + // bip16 specifies that the public key script is of a special form that + // indicates it is a BIP16 pay-to-script-hash and therefore the + // execution must be treated as such. + // + // sigCache caches the results of signature verifications. This is useful + // since transaction scripts are often executed more than once from various + // contexts (e.g. new block templates, when transactions are first seen + // prior to being mined, part of full block verification, etc). + // + // hashCache caches the midstate of segwit v0 and v1 sighashes to + // optimize worst-case hashing complexity. + // + // prevOutFetcher is used to look up all the previous output of + // taproot transactions, as that information is hashed into the + // sighash digest for such inputs. + flags txscript.ScriptFlags + tx wire.MsgTx + txIdx int + version uint16 + bip16 bool + sigCache *txscript.SigCache + hashCache *txscript.TxSigHashes + prevOutFetcher txscript.PrevOutputFetcher + + // The following fields handle keeping track of the current execution state + // of the engine. + // + // scripts houses the raw scripts that are executed by the engine. This + // includes the signature script as well as the public key script. It also + // includes the redeem script in the case of pay-to-script-hash. + // + // scriptIdx tracks the index into the scripts array for the current program + // counter. + // + // opcodeIdx tracks the number of the opcode within the current script for + // the current program counter. Note that it differs from the actual byte + // index into the script and is really only used for disassembly purposes. + // + // lastCodeSep specifies the position within the current script of the last + // OP_CODESEPARATOR. + // + // tokenizer provides the token stream of the current script being executed + // and doubles as state tracking for the program counter within the script. + // + // savedFirstStack keeps a copy of the stack from the first script when + // performing pay-to-script-hash execution. + // + // dstack is the primary data stack the various opcodes push and pop data + // to and from during execution. + // + // astack is the alternate data stack the various opcodes push and pop data + // to and from during execution. + // + // condStack tracks the conditional execution state with support for + // multiple nested conditional execution opcodes. + // + // numOps tracks the total number of non-push operations in a script and is + // primarily used to enforce maximum limits. + scripts [][]byte + scriptIdx int + opcodeIdx int + lastCodeSep int + tokenizer ScriptTokenizer + savedFirstStack [][]byte + dstack stack + astack stack + condStack []int + numOps int + witnessVersion int + witnessProgram []byte + inputAmount int64 + taprootCtx *taprootExecutionCtx + + // stepCallback is an optional function that will be called every time + // a step has been performed during script execution. + // + // NOTE: This is only meant to be used in debugging, and SHOULD NOT BE + // USED during regular operation. + stepCallback func(*StepInfo) error +} + +// StepInfo houses the current VM state information that is passed back to the +// stepCallback during script execution. +type StepInfo struct { + // ScriptIndex is the index of the script currently being executed by + // the Engine. + ScriptIndex int + + // OpcodeIndex is the index of the next opcode that will be executed. + // In case the execution has completed, the opcode index will be + // incrementet beyond the number of the current script's opcodes. This + // indicates no new script is being executed, and execution is done. + OpcodeIndex int + + // Stack is the Engine's current content on the stack: + Stack [][]byte + + // AltStack is the Engine's current content on the alt stack. + AltStack [][]byte +} + +// hasFlag returns whether the script engine instance has the passed flag set. +func (vm *Engine) hasFlag(flag txscript.ScriptFlags) bool { + return vm.flags&flag == flag +} + +// isBranchExecuting returns whether or not the current conditional branch is +// actively executing. For example, when the data stack has an OP_FALSE on it +// and an OP_IF is encountered, the branch is inactive until an OP_ELSE or +// OP_ENDIF is encountered. It properly handles nested conditionals. +func (vm *Engine) isBranchExecuting() bool { + if len(vm.condStack) == 0 { + return true + } + return vm.condStack[len(vm.condStack)-1] == txscript.OpCondTrue +} + +// isOpcodeDisabled returns whether or not the opcode is disabled and thus is +// always bad to see in the instruction stream (even if turned off by a +// conditional). +func isOpcodeDisabled(opcode byte) bool { + return false +} + +// isOpcodeAlwaysIllegal returns whether or not the opcode is always illegal +// when passed over by the program counter even if in a non-executed branch (it +// isn't a coincidence that they are conditionals). +func isOpcodeAlwaysIllegal(opcode byte) bool { + switch opcode { + case OP_VERIF: + return true + case OP_VERNOTIF: + return true + default: + return false + } +} + +// isOpcodeConditional returns whether or not the opcode is a conditional opcode +// which changes the conditional execution stack when executed. +func isOpcodeConditional(opcode byte) bool { + switch opcode { + case OP_IF: + return true + case OP_NOTIF: + return true + case OP_ELSE: + return true + case OP_ENDIF: + return true + default: + return false + } +} + +// checkMinimalDataPush returns whether or not the provided opcode is the +// smallest possible way to represent the given data. For example, the value 15 +// could be pushed with OP_DATA_1 15 (among other variations); however, OP_15 is +// a single opcode that represents the same value and is only a single byte +// versus two bytes. +func checkMinimalDataPush(op *opcode, data []byte) error { + opcodeVal := op.value + dataLen := len(data) + switch { + case dataLen == 0 && opcodeVal != OP_0: + str := fmt.Sprintf("zero length data push is encoded with opcode %s "+ + "instead of OP_0", op.name) + return scriptError(txscript.ErrMinimalData, str) + case dataLen == 1 && data[0] >= 1 && data[0] <= 16: + if opcodeVal != OP_1+data[0]-1 { + // Should have used OP_1 .. OP_16 + str := fmt.Sprintf("data push of the value %d encoded with opcode "+ + "%s instead of OP_%d", data[0], op.name, data[0]) + return scriptError(txscript.ErrMinimalData, str) + } + case dataLen == 1 && data[0] == 0x81: + if opcodeVal != OP_1NEGATE { + str := fmt.Sprintf("data push of the value -1 encoded with opcode "+ + "%s instead of OP_1NEGATE", op.name) + return scriptError(txscript.ErrMinimalData, str) + } + case dataLen <= 75: + if int(opcodeVal) != dataLen { + // Should have used a direct push + str := fmt.Sprintf("data push of %d bytes encoded with opcode %s "+ + "instead of OP_DATA_%d", dataLen, op.name, dataLen) + return scriptError(txscript.ErrMinimalData, str) + } + case dataLen <= 255: + if opcodeVal != OP_PUSHDATA1 { + str := fmt.Sprintf("data push of %d bytes encoded with opcode %s "+ + "instead of OP_PUSHDATA1", dataLen, op.name) + return scriptError(txscript.ErrMinimalData, str) + } + case dataLen <= 65535: + if opcodeVal != OP_PUSHDATA2 { + str := fmt.Sprintf("data push of %d bytes encoded with opcode %s "+ + "instead of OP_PUSHDATA2", dataLen, op.name) + return scriptError(txscript.ErrMinimalData, str) + } + } + return nil +} + +// executeOpcode performs execution on the passed opcode. It takes into account +// whether or not it is hidden by conditionals, but some rules still must be +// tested in this case. +func (vm *Engine) executeOpcode(op *opcode, data []byte) error { + // Disabled opcodes are fail on program counter. + if isOpcodeDisabled(op.value) { + str := fmt.Sprintf("attempt to execute disabled opcode %s", op.name) + return scriptError(txscript.ErrDisabledOpcode, str) + } + + // Always-illegal opcodes are fail on program counter. + if isOpcodeAlwaysIllegal(op.value) { + str := fmt.Sprintf("attempt to execute reserved opcode %s", op.name) + return scriptError(txscript.ErrReservedOpcode, str) + } + + // Note that this includes OP_RESERVED which counts as a push operation. + if vm.taprootCtx == nil && op.value > OP_16 { + vm.numOps++ + if vm.numOps > txscript.MaxOpsPerScript { + str := fmt.Sprintf("exceeded max operation limit of %d", + txscript.MaxOpsPerScript) + return scriptError(txscript.ErrTooManyOperations, str) + } + + } else if len(data) > txscript.MaxScriptElementSize { + str := fmt.Sprintf("element size %d exceeds max allowed size %d", + len(data), txscript.MaxScriptElementSize) + return scriptError(txscript.ErrElementTooBig, str) + } + + // Nothing left to do when this is not a conditional opcode and it is + // not in an executing branch. + if !vm.isBranchExecuting() && !isOpcodeConditional(op.value) { + return nil + } + + // Ensure all executed data push opcodes use the minimal encoding when + // the minimal data verification flag is set. + if vm.dstack.verifyMinimalData && vm.isBranchExecuting() && op.value <= OP_PUSHDATA4 { + if err := checkMinimalDataPush(op, data); err != nil { + return err + } + } + + return op.opfunc(op, data, vm) +} + +// checkValidPC returns an error if the current script position is not valid for +// execution. +func (vm *Engine) checkValidPC() error { + if vm.scriptIdx >= len(vm.scripts) { + str := fmt.Sprintf("script index %d beyond total scripts %d", + vm.scriptIdx, len(vm.scripts)) + return scriptError(txscript.ErrInvalidProgramCounter, str) + } + return nil +} + +// isWitnessVersionActive returns true if a witness program was extracted +// during the initialization of the Engine, and the program's version matches +// the specified version. +func (vm *Engine) isWitnessVersionActive(version uint) bool { + return vm.witnessProgram != nil && uint(vm.witnessVersion) == version +} + +// verifyWitnessProgram validates the stored witness program using the passed +// witness as input. +func (vm *Engine) verifyWitnessProgram(witness wire.TxWitness) error { + switch { + case vm.isWitnessVersionActive(txscript.BaseSegwitWitnessVersion): + return fmt.Errorf("arkscript engine only supports taproot") + case vm.isWitnessVersionActive(txscript.TaprootWitnessVersion) && + len(vm.witnessProgram) == payToTaprootDataSize && !vm.bip16: + + // If taproot isn't currently active, then we'll return a + // success here in place as we don't apply the new rules unless + // the flag flips, as governed by the version bits deployment. + if !vm.hasFlag(txscript.ScriptVerifyTaproot) { + return nil + } + + // If there're no stack elements at all, then this is an + // invalid spend. + if len(witness) == 0 { + return scriptError(txscript.ErrWitnessProgramEmpty, "witness "+ + "program empty passed empty witness") + } + + // At this point, we know taproot is active, so we'll populate + // the taproot execution context. + vm.taprootCtx = newTaprootExecutionCtx( + int32(witness.SerializeSize()), + ) + + // If we can detect the annex, then drop that off the stack, + // we'll only need it to compute the sighash later. + if isAnnexedWitness(witness) { + vm.taprootCtx.annex, _ = extractAnnex(witness) + + // Snip the annex off the end of the witness stack. + witness = witness[:len(witness)-1] + } + + // From here, we'll either be validating a normal key spend, or + // a spend from the tap script leaf using a committed leaf. + switch { + // If there's only a single element left on the stack (the + // signature), then we'll apply the normal top-level schnorr + // signature verification. + case len(witness) == 1: + // As we only have a single element left (after maybe + // removing the annex), we'll do normal taproot + // keyspend validation. + rawSig := witness[0] + err := txscript.VerifyTaprootKeySpend( + vm.witnessProgram, rawSig, &vm.tx, vm.txIdx, + vm.prevOutFetcher, vm.hashCache, vm.sigCache, + ) + if err != nil { + // TODO(roasbeef): proper error + return err + } + + // TODO(roasbeef): or remove the other items from the stack? + vm.taprootCtx.mustSucceed = true + return nil + + // Otherwise, we need to attempt full tapscript leaf + // verification in place. + default: + // First, attempt to parse the control block, if this + // isn't formatted properly, then we'll end execution + // right here. + controlBlock, err := txscript.ParseControlBlock( + witness[len(witness)-1], + ) + if err != nil { + return err + } + + // Now that we know the control block is valid, we'll + // verify the top-level taproot commitment, which + // proves that the specified script was committed to in + // the merkle tree. + witnessScript := witness[len(witness)-2] + err = txscript.VerifyTaprootLeafCommitment( + controlBlock, vm.witnessProgram, witnessScript, + ) + if err != nil { + return err + } + + // Before we proceed with normal execution, check the + // leaf version of the script, as if the policy flag is + // active, then we should only allow the base leaf + // version. + if controlBlock.LeafVersion != txscript.BaseLeafVersion { + switch { + case vm.hasFlag(txscript.ScriptVerifyDiscourageUpgradeableTaprootVersion): + errStr := fmt.Sprintf("tapscript is attempting "+ + "to use version: %v", controlBlock.LeafVersion) + return scriptError( + txscript.ErrDiscourageUpgradeableTaprootVersion, errStr, + ) + default: + // If the policy flag isn't active, + // then execution succeeds here as we + // don't know the rules of the future + // leaf versions. + vm.taprootCtx.mustSucceed = true + return nil + } + } + + // Now that we know we don't have any op success + // fields, ensure that the script parses properly. + // + // TODO(roasbeef): combine w/ the above? + err = checkScriptParses(vm.version, witnessScript) + if err != nil { + return err + } + + // Now that we know the script parses, and we have a + // valid leaf version, we'll save the tapscript hash of + // the leaf, as we need that for signature validation + // later. + vm.taprootCtx.tapLeafHash = txscript.NewBaseTapLeaf( + witnessScript, + ).TapHash() + + // Otherwise, we'll now "recurse" one level deeper, and + // set the remaining witness (leaving off the annex and + // the witness script) as the execution stack, and + // enter further execution. + vm.scripts = append(vm.scripts, witnessScript) + vm.SetStack(witness[:len(witness)-2]) + } + + case vm.hasFlag(txscript.ScriptVerifyDiscourageUpgradeableWitnessProgram): + errStr := fmt.Sprintf("new witness program versions "+ + "invalid: %v", vm.witnessProgram) + + return scriptError(txscript.ErrDiscourageUpgradableWitnessProgram, errStr) + default: + // If we encounter an unknown witness program version and we + // aren't discouraging future unknown witness based soft-forks, + // then we de-activate the segwit behavior within the VM for + // the remainder of execution. + vm.witnessProgram = nil + } + + // TODO(roasbeef): other sanity checks here + switch { + + // In addition to the normal script element size limits, taproot also + // enforces a limit on the max _starting_ stack size. + case vm.isWitnessVersionActive(txscript.TaprootWitnessVersion): + if vm.dstack.Depth() > txscript.MaxStackSize { + str := fmt.Sprintf("tapscript stack size %d > max allowed %d", + vm.dstack.Depth(), txscript.MaxStackSize) + return scriptError(txscript.ErrStackOverflow, str) + } + + fallthrough + case vm.isWitnessVersionActive(txscript.BaseSegwitWitnessVersion): + // All elements within the witness stack must not be greater + // than the maximum bytes which are allowed to be pushed onto + // the stack. + for _, witElement := range vm.GetStack() { + if len(witElement) > txscript.MaxScriptElementSize { + str := fmt.Sprintf("element size %d exceeds "+ + "max allowed size %d", len(witElement), + txscript.MaxScriptElementSize) + return scriptError(txscript.ErrElementTooBig, str) + } + } + + return nil + } + + return nil +} + +// DisasmPC returns the string for the disassembly of the opcode that will be +// next to execute when Step is called. +func (vm *Engine) DisasmPC() (string, error) { + if err := vm.checkValidPC(); err != nil { + return "", err + } + + // Create a copy of the current tokenizer and parse the next opcode in the + // copy to avoid mutating the current one. + peekTokenizer := vm.tokenizer + if !peekTokenizer.Next() { + // Note that due to the fact that all scripts are checked for parse + // failures before this code ever runs, there should never be an error + // here, but check again to be safe in case a refactor breaks that + // assumption or new script versions are introduced with different + // semantics. + if err := peekTokenizer.Err(); err != nil { + return "", err + } + + // Note that this should be impossible to hit in practice because the + // only way it could happen would be for the final opcode of a script to + // already be parsed without the script index having been updated, which + // is not the case since stepping the script always increments the + // script index when parsing and executing the final opcode of a script. + // + // However, check again to be safe in case a refactor breaks that + // assumption or new script versions are introduced with different + // semantics. + str := fmt.Sprintf("program counter beyond script index %d (bytes %x)", + vm.scriptIdx, vm.scripts[vm.scriptIdx]) + return "", scriptError(txscript.ErrInvalidProgramCounter, str) + } + + var buf strings.Builder + disasmOpcode(&buf, peekTokenizer.op, peekTokenizer.Data(), false) + return fmt.Sprintf("%02x:%04x: %s", vm.scriptIdx, vm.opcodeIdx, + buf.String()), nil +} + +// DisasmScript returns the disassembly string for the script at the requested +// offset index. Index 0 is the signature script and 1 is the public key +// script. In the case of pay-to-script-hash, index 2 is the redeem script once +// the execution has progressed far enough to have successfully verified script +// hash and thus add the script to the scripts to execute. +func (vm *Engine) DisasmScript(idx int) (string, error) { + if idx >= len(vm.scripts) { + str := fmt.Sprintf("script index %d >= total scripts %d", idx, + len(vm.scripts)) + return "", scriptError(txscript.ErrInvalidIndex, str) + } + + var disbuf strings.Builder + script := vm.scripts[idx] + tokenizer := MakeScriptTokenizer(vm.version, script) + var opcodeIdx int + for tokenizer.Next() { + disbuf.WriteString(fmt.Sprintf("%02x:%04x: ", idx, opcodeIdx)) + disasmOpcode(&disbuf, tokenizer.op, tokenizer.Data(), false) + disbuf.WriteByte('\n') + opcodeIdx++ + } + return disbuf.String(), tokenizer.Err() +} + +// CheckErrorCondition returns nil if the running script has ended and was +// successful, leaving a a true boolean on the stack. An error otherwise, +// including if the script has not finished. +func (vm *Engine) CheckErrorCondition(finalScript bool) error { + if vm.taprootCtx != nil && vm.taprootCtx.mustSucceed { + return nil + } + + // Check execution is actually done by ensuring the script index is after + // the final script in the array script. + if vm.scriptIdx < len(vm.scripts) { + return scriptError(txscript.ErrScriptUnfinished, + "error check when script unfinished") + } + + // If we're in version zero witness execution mode, and this was the + // final script, then the stack MUST be clean in order to maintain + // compatibility with BIP16. + if finalScript && vm.isWitnessVersionActive(txscript.BaseSegwitWitnessVersion) && + vm.dstack.Depth() != 1 { + return scriptError(txscript.ErrEvalFalse, "witness program must "+ + "have clean stack") + } + + // The final script must end with exactly one data stack item when the + // verify clean stack flag is set. Otherwise, there must be at least one + // data stack item in order to interpret it as a boolean. + cleanStackActive := vm.hasFlag(txscript.ScriptVerifyCleanStack) || vm.taprootCtx != nil + if finalScript && cleanStackActive && vm.dstack.Depth() != 1 { + + str := fmt.Sprintf("stack must contain exactly one item (contains %d)", + vm.dstack.Depth()) + return scriptError(txscript.ErrCleanStack, str) + } else if vm.dstack.Depth() < 1 { + return scriptError(txscript.ErrEmptyStack, + "stack empty at end of script execution") + } + + v, err := vm.dstack.PopBool() + if err != nil { + return err + } + if !v { + return scriptError(txscript.ErrEvalFalse, + "false stack entry at end of script execution") + } + return nil +} + +// Step executes the next instruction and moves the program counter to the next +// opcode in the script, or the next script if the current has ended. Step will +// return true in the case that the last opcode was successfully executed. +// +// The result of calling Step or any other method is undefined if an error is +// returned. +func (vm *Engine) Step() (done bool, err error) { + // Verify the engine is pointing to a valid program counter. + if err := vm.checkValidPC(); err != nil { + return true, err + } + + // Attempt to parse the next opcode from the current script. + if !vm.tokenizer.Next() { + // Note that due to the fact that all scripts are checked for parse + // failures before this code ever runs, there should never be an error + // here, but check again to be safe in case a refactor breaks that + // assumption or new script versions are introduced with different + // semantics. + if err := vm.tokenizer.Err(); err != nil { + return false, err + } + + str := fmt.Sprintf("attempt to step beyond script index %d (bytes %x)", + vm.scriptIdx, vm.scripts[vm.scriptIdx]) + return true, scriptError(txscript.ErrInvalidProgramCounter, str) + } + + // Execute the opcode while taking into account several things such as + // disabled opcodes, illegal opcodes, maximum allowed operations per script, + // maximum script element sizes, and conditionals. + err = vm.executeOpcode(vm.tokenizer.op, vm.tokenizer.Data()) + if err != nil { + return true, err + } + + // The number of elements in the combination of the data and alt stacks + // must not exceed the maximum number of stack elements allowed. + combinedStackSize := vm.dstack.Depth() + vm.astack.Depth() + if combinedStackSize > txscript.MaxStackSize { + str := fmt.Sprintf("combined stack size %d > max allowed %d", + combinedStackSize, txscript.MaxStackSize) + return false, scriptError(txscript.ErrStackOverflow, str) + } + + // Prepare for next instruction. + vm.opcodeIdx++ + if vm.tokenizer.Done() { + // Illegal to have a conditional that straddles two scripts. + if len(vm.condStack) != 0 { + return false, scriptError(txscript.ErrUnbalancedConditional, + "end of script reached in conditional execution") + } + + // Alt stack doesn't persist between scripts. + _ = vm.astack.DropN(vm.astack.Depth()) + + // The number of operations is per script. + vm.numOps = 0 + + // Reset the opcode index for the next script. + vm.opcodeIdx = 0 + + // Advance to the next script as needed. + switch { + case vm.scriptIdx == 0 && vm.bip16: + vm.scriptIdx++ + vm.savedFirstStack = vm.GetStack() + + case vm.scriptIdx == 1 && vm.bip16: + // Put us past the end for CheckErrorCondition() + vm.scriptIdx++ + + // Check script ran successfully. + err := vm.CheckErrorCondition(false) + if err != nil { + return false, err + } + + // Obtain the redeem script from the first stack and ensure it + // parses. + script := vm.savedFirstStack[len(vm.savedFirstStack)-1] + if err := checkScriptParses(vm.version, script); err != nil { + return false, err + } + vm.scripts = append(vm.scripts, script) + + // Set stack to be the stack from first script minus the redeem + // script itself + vm.SetStack(vm.savedFirstStack[:len(vm.savedFirstStack)-1]) + + case vm.scriptIdx == 1 && vm.witnessProgram != nil, + vm.scriptIdx == 2 && vm.witnessProgram != nil && vm.bip16: // np2sh + + vm.scriptIdx++ + + witness := vm.tx.TxIn[vm.txIdx].Witness + if err := vm.verifyWitnessProgram(witness); err != nil { + return false, err + } + + default: + vm.scriptIdx++ + } + + // Skip empty scripts. + if vm.scriptIdx < len(vm.scripts) && len(vm.scripts[vm.scriptIdx]) == 0 { + vm.scriptIdx++ + } + + vm.lastCodeSep = 0 + if vm.scriptIdx >= len(vm.scripts) { + return true, nil + } + + // Finally, update the current tokenizer used to parse through scripts + // one opcode at a time to start from the beginning of the new script + // associated with the program counter. + vm.tokenizer = MakeScriptTokenizer(vm.version, vm.scripts[vm.scriptIdx]) + } + + return false, nil +} + +// copyStack makes a deep copy of the provided slice. +func copyStack(stk [][]byte) [][]byte { + c := make([][]byte, len(stk)) + for i := range stk { + c[i] = make([]byte, len(stk[i])) + copy(c[i][:], stk[i][:]) + } + + return c +} + +// Execute will execute all scripts in the script engine and return either nil +// for successful validation or an error if one occurred. +func (vm *Engine) Execute() (err error) { + // All script versions other than 0 currently execute without issue, + // making all outputs to them anyone can pay. In the future this + // will allow for the addition of new scripting languages. + if vm.version != 0 { + return nil + } + + // If the stepCallback is set, we start by making a call back with the + // initial engine state. + var stepInfo *StepInfo + if vm.stepCallback != nil { + stepInfo = &StepInfo{ + ScriptIndex: vm.scriptIdx, + OpcodeIndex: vm.opcodeIdx, + Stack: copyStack(vm.dstack.stk), + AltStack: copyStack(vm.astack.stk), + } + err := vm.stepCallback(stepInfo) + if err != nil { + return err + } + } + + done := false + for !done { + done, err = vm.Step() + if err != nil { + return err + } + + if vm.stepCallback != nil { + scriptIdx := vm.scriptIdx + opcodeIdx := vm.opcodeIdx + + // In case the execution has completed, we keep the + // current script index while increasing the opcode + // index. This is to indicate that no new script is + // being executed. + if done { + scriptIdx = stepInfo.ScriptIndex + opcodeIdx = stepInfo.OpcodeIndex + 1 + } + + stepInfo = &StepInfo{ + ScriptIndex: scriptIdx, + OpcodeIndex: opcodeIdx, + Stack: copyStack(vm.dstack.stk), + AltStack: copyStack(vm.astack.stk), + } + err := vm.stepCallback(stepInfo) + if err != nil { + return err + } + } + } + + return vm.CheckErrorCondition(true) +} + +// subScript returns the script since the last OP_CODESEPARATOR. +func (vm *Engine) subScript() []byte { + return vm.scripts[vm.scriptIdx][vm.lastCodeSep:] +} + +// checkHashTypeEncoding returns whether or not the passed hashtype adheres to +// the strict encoding requirements if enabled. +func (vm *Engine) checkHashTypeEncoding(hashType txscript.SigHashType) error { + if !vm.hasFlag(txscript.ScriptVerifyStrictEncoding) { + return nil + } + + sigHashType := hashType & ^txscript.SigHashAnyOneCanPay + if sigHashType < txscript.SigHashAll || sigHashType > txscript.SigHashSingle { + str := fmt.Sprintf("invalid hash type 0x%x", hashType) + return scriptError(txscript.ErrInvalidSigHashType, str) + } + return nil +} + +// checkPubKeyEncoding returns whether or not the passed public key adheres to +// the strict encoding requirements if enabled. +func (vm *Engine) checkPubKeyEncoding(pubKey []byte) error { + if vm.hasFlag(txscript.ScriptVerifyWitnessPubKeyType) && + vm.isWitnessVersionActive(txscript.BaseSegwitWitnessVersion) && + !btcec.IsCompressedPubKey(pubKey) { + + str := "only compressed keys are accepted post-segwit" + return scriptError(txscript.ErrWitnessPubKeyType, str) + } + + if !vm.hasFlag(txscript.ScriptVerifyStrictEncoding) { + return nil + } + + if len(pubKey) == 33 && (pubKey[0] == 0x02 || pubKey[0] == 0x03) { + // Compressed + return nil + } + if len(pubKey) == 65 && pubKey[0] == 0x04 { + // Uncompressed + return nil + } + + return scriptError(txscript.ErrPubKeyType, "unsupported public key type") +} + +// checkSignatureEncoding returns whether or not the passed signature adheres to +// the strict encoding requirements if enabled. +func (vm *Engine) checkSignatureEncoding(sig []byte) error { + if !vm.hasFlag(txscript.ScriptVerifyDERSignatures) && + !vm.hasFlag(txscript.ScriptVerifyLowS) && + !vm.hasFlag(txscript.ScriptVerifyStrictEncoding) { + + return nil + } + + // The format of a DER encoded signature is as follows: + // + // 0x30 0x02 0x02 + // - 0x30 is the ASN.1 identifier for a sequence + // - Total length is 1 byte and specifies length of all remaining data + // - 0x02 is the ASN.1 identifier that specifies an integer follows + // - Length of R is 1 byte and specifies how many bytes R occupies + // - R is the arbitrary length big-endian encoded number which + // represents the R value of the signature. DER encoding dictates + // that the value must be encoded using the minimum possible number + // of bytes. This implies the first byte can only be null if the + // highest bit of the next byte is set in order to prevent it from + // being interpreted as a negative number. + // - 0x02 is once again the ASN.1 integer identifier + // - Length of S is 1 byte and specifies how many bytes S occupies + // - S is the arbitrary length big-endian encoded number which + // represents the S value of the signature. The encoding rules are + // identical as those for R. + const ( + asn1SequenceID = 0x30 + asn1IntegerID = 0x02 + + // minSigLen is the minimum length of a DER encoded signature and is + // when both R and S are 1 byte each. + // + // 0x30 + <1-byte> + 0x02 + 0x01 + + 0x2 + 0x01 + + minSigLen = 8 + + // maxSigLen is the maximum length of a DER encoded signature and is + // when both R and S are 33 bytes each. It is 33 bytes because a + // 256-bit integer requires 32 bytes and an additional leading null byte + // might required if the high bit is set in the value. + // + // 0x30 + <1-byte> + 0x02 + 0x21 + <33 bytes> + 0x2 + 0x21 + <33 bytes> + maxSigLen = 72 + + // sequenceOffset is the byte offset within the signature of the + // expected ASN.1 sequence identifier. + sequenceOffset = 0 + + // dataLenOffset is the byte offset within the signature of the expected + // total length of all remaining data in the signature. + dataLenOffset = 1 + + // rTypeOffset is the byte offset within the signature of the ASN.1 + // identifier for R and is expected to indicate an ASN.1 integer. + rTypeOffset = 2 + + // rLenOffset is the byte offset within the signature of the length of + // R. + rLenOffset = 3 + + // rOffset is the byte offset within the signature of R. + rOffset = 4 + ) + + // The signature must adhere to the minimum and maximum allowed length. + sigLen := len(sig) + if sigLen < minSigLen { + str := fmt.Sprintf("malformed signature: too short: %d < %d", sigLen, + minSigLen) + return scriptError(txscript.ErrSigTooShort, str) + } + if sigLen > maxSigLen { + str := fmt.Sprintf("malformed signature: too long: %d > %d", sigLen, + maxSigLen) + return scriptError(txscript.ErrSigTooLong, str) + } + + // The signature must start with the ASN.1 sequence identifier. + if sig[sequenceOffset] != asn1SequenceID { + str := fmt.Sprintf("malformed signature: format has wrong type: %#x", + sig[sequenceOffset]) + return scriptError(txscript.ErrSigInvalidSeqID, str) + } + + // The signature must indicate the correct amount of data for all elements + // related to R and S. + if int(sig[dataLenOffset]) != sigLen-2 { + str := fmt.Sprintf("malformed signature: bad length: %d != %d", + sig[dataLenOffset], sigLen-2) + return scriptError(txscript.ErrSigInvalidDataLen, str) + } + + // Calculate the offsets of the elements related to S and ensure S is inside + // the signature. + // + // rLen specifies the length of the big-endian encoded number which + // represents the R value of the signature. + // + // sTypeOffset is the offset of the ASN.1 identifier for S and, like its R + // counterpart, is expected to indicate an ASN.1 integer. + // + // sLenOffset and sOffset are the byte offsets within the signature of the + // length of S and S itself, respectively. + rLen := int(sig[rLenOffset]) + sTypeOffset := rOffset + rLen + sLenOffset := sTypeOffset + 1 + if sTypeOffset >= sigLen { + str := "malformed signature: S type indicator missing" + return scriptError(txscript.ErrSigMissingSTypeID, str) + } + if sLenOffset >= sigLen { + str := "malformed signature: S length missing" + return scriptError(txscript.ErrSigMissingSLen, str) + } + + // The lengths of R and S must match the overall length of the signature. + // + // sLen specifies the length of the big-endian encoded number which + // represents the S value of the signature. + sOffset := sLenOffset + 1 + sLen := int(sig[sLenOffset]) + if sOffset+sLen != sigLen { + str := "malformed signature: invalid S length" + return scriptError(txscript.ErrSigInvalidSLen, str) + } + + // R elements must be ASN.1 integers. + if sig[rTypeOffset] != asn1IntegerID { + str := fmt.Sprintf("malformed signature: R integer marker: %#x != %#x", + sig[rTypeOffset], asn1IntegerID) + return scriptError(txscript.ErrSigInvalidRIntID, str) + } + + // Zero-length integers are not allowed for R. + if rLen == 0 { + str := "malformed signature: R length is zero" + return scriptError(txscript.ErrSigZeroRLen, str) + } + + // R must not be negative. + if sig[rOffset]&0x80 != 0 { + str := "malformed signature: R is negative" + return scriptError(txscript.ErrSigNegativeR, str) + } + + // Null bytes at the start of R are not allowed, unless R would otherwise be + // interpreted as a negative number. + if rLen > 1 && sig[rOffset] == 0x00 && sig[rOffset+1]&0x80 == 0 { + str := "malformed signature: R value has too much padding" + return scriptError(txscript.ErrSigTooMuchRPadding, str) + } + + // S elements must be ASN.1 integers. + if sig[sTypeOffset] != asn1IntegerID { + str := fmt.Sprintf("malformed signature: S integer marker: %#x != %#x", + sig[sTypeOffset], asn1IntegerID) + return scriptError(txscript.ErrSigInvalidSIntID, str) + } + + // Zero-length integers are not allowed for S. + if sLen == 0 { + str := "malformed signature: S length is zero" + return scriptError(txscript.ErrSigZeroSLen, str) + } + + // S must not be negative. + if sig[sOffset]&0x80 != 0 { + str := "malformed signature: S is negative" + return scriptError(txscript.ErrSigNegativeS, str) + } + + // Null bytes at the start of S are not allowed, unless S would otherwise be + // interpreted as a negative number. + if sLen > 1 && sig[sOffset] == 0x00 && sig[sOffset+1]&0x80 == 0 { + str := "malformed signature: S value has too much padding" + return scriptError(txscript.ErrSigTooMuchSPadding, str) + } + + // Verify the S value is <= half the order of the curve. This check is done + // because when it is higher, the complement modulo the order can be used + // instead which is a shorter encoding by 1 byte. Further, without + // enforcing this, it is possible to replace a signature in a valid + // transaction with the complement while still being a valid signature that + // verifies. This would result in changing the transaction hash and thus is + // a source of malleability. + if vm.hasFlag(txscript.ScriptVerifyLowS) { + sValue := new(big.Int).SetBytes(sig[sOffset : sOffset+sLen]) + if sValue.Cmp(halfOrder) > 0 { + return scriptError(txscript.ErrSigHighS, "signature is not canonical due "+ + "to unnecessarily high S value") + } + } + + return nil +} + +// getStack returns the contents of stack as a byte array bottom up +func getStack(stack *stack) [][]byte { + array := make([][]byte, stack.Depth()) + for i := range array { + // PeekByteArray can't fail due to overflow, already checked + array[len(array)-i-1], _ = stack.PeekByteArray(int32(i)) + } + return array +} + +// setStack sets the stack to the contents of the array where the last item in +// the array is the top item in the stack. +func setStack(stack *stack, data [][]byte) { + // This can not error. Only errors are for invalid arguments. + _ = stack.DropN(stack.Depth()) + + for i := range data { + stack.PushByteArray(data[i]) + } +} + +// GetStack returns the contents of the primary stack as an array. where the +// last item in the array is the top of the stack. +func (vm *Engine) GetStack() [][]byte { + return getStack(&vm.dstack) +} + +// SetStack sets the contents of the primary stack to the contents of the +// provided array where the last item in the array will be the top of the stack. +func (vm *Engine) SetStack(data [][]byte) { + setStack(&vm.dstack, data) +} + +// GetAltStack returns the contents of the alternate stack as an array where the +// last item in the array is the top of the stack. +func (vm *Engine) GetAltStack() [][]byte { + return getStack(&vm.astack) +} + +// SetAltStack sets the contents of the alternate stack to the contents of the +// provided array where the last item in the array will be the top of the stack. +func (vm *Engine) SetAltStack(data [][]byte) { + setStack(&vm.astack, data) +} + +// NewEngine returns a new script engine for the provided public key script, +// transaction, and input index. The flags modify the behavior of the script +// engine according to the description provided by each flag. +func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags txscript.ScriptFlags, + sigCache *txscript.SigCache, hashCache *txscript.TxSigHashes, inputAmount int64, + prevOutFetcher txscript.PrevOutputFetcher) (*Engine, error) { + + const scriptVersion = 0 + + // The provided transaction input index must refer to a valid input. + if txIdx < 0 || txIdx >= len(tx.TxIn) { + str := fmt.Sprintf("transaction input index %d is negative or "+ + ">= %d", txIdx, len(tx.TxIn)) + return nil, scriptError(txscript.ErrInvalidIndex, str) + } + scriptSig := tx.TxIn[txIdx].SignatureScript + + // When both the signature script and public key script are empty the result + // is necessarily an error since the stack would end up being empty which is + // equivalent to a false top element. Thus, just return the relevant error + // now as an optimization. + if len(scriptSig) == 0 && len(scriptPubKey) == 0 { + return nil, scriptError(txscript.ErrEvalFalse, + "false stack entry at end of script execution") + } + + // The clean stack flag (ScriptVerifyCleanStack) is not allowed without + // either the pay-to-script-hash (P2SH) evaluation (ScriptBip16) + // flag or the Segregated Witness (ScriptVerifyWitness) flag. + // + // Recall that evaluating a P2SH script without the flag set results in + // non-P2SH evaluation which leaves the P2SH inputs on the stack. + // Thus, allowing the clean stack flag without the P2SH flag would make + // it possible to have a situation where P2SH would not be a soft fork + // when it should be. The same goes for segwit which will pull in + // additional scripts for execution from the witness stack. + vm := Engine{ + flags: flags, + sigCache: sigCache, + hashCache: hashCache, + inputAmount: inputAmount, + prevOutFetcher: prevOutFetcher, + } + if vm.hasFlag(txscript.ScriptVerifyCleanStack) && (!vm.hasFlag(txscript.ScriptBip16) && + !vm.hasFlag(txscript.ScriptVerifyWitness)) { + return nil, scriptError(txscript.ErrInvalidFlags, + "invalid flags combination") + } + + // The signature script must only contain data pushes when the + // associated flag is set. + if vm.hasFlag(txscript.ScriptVerifySigPushOnly) && !txscript.IsPushOnlyScript(scriptSig) { + return nil, scriptError(txscript.ErrNotPushOnly, + "signature script is not push only") + } + + // The signature script must only contain data pushes for PS2H which is + // determined based on the form of the public key script. + if vm.hasFlag(txscript.ScriptBip16) && txscript.IsPayToScriptHash(scriptPubKey) { + // Only accept input scripts that push data for P2SH. + // Notice that the push only checks have already been done when + // the flag to verify signature scripts are push only is set + // above, so avoid checking again. + alreadyChecked := vm.hasFlag(txscript.ScriptVerifySigPushOnly) + if !alreadyChecked && !txscript.IsPushOnlyScript(scriptSig) { + return nil, scriptError(txscript.ErrNotPushOnly, + "pay to script hash is not push only") + } + vm.bip16 = true + } + + // The engine stores the scripts using a slice. This allows multiple + // scripts to be executed in sequence. For example, with a + // pay-to-script-hash transaction, there will be ultimately be a third + // script to execute. + scripts := [][]byte{scriptSig, scriptPubKey} + for _, scr := range scripts { + if len(scr) > txscript.MaxScriptSize { + str := fmt.Sprintf("script size %d is larger than max allowed "+ + "size %d", len(scr), txscript.MaxScriptSize) + return nil, scriptError(txscript.ErrScriptTooBig, str) + } + + const scriptVersion = 0 + if err := checkScriptParses(scriptVersion, scr); err != nil { + return nil, err + } + } + vm.scripts = scripts + + // Advance the program counter to the public key script if the signature + // script is empty since there is nothing to execute for it in that case. + if len(scriptSig) == 0 { + vm.scriptIdx++ + } + if vm.hasFlag(txscript.ScriptVerifyMinimalData) { + vm.dstack.verifyMinimalData = true + vm.astack.verifyMinimalData = true + } + + // Check to see if we should execute in witness verification mode + // according to the set flags. We check both the pkScript, and sigScript + // here since in the case of nested p2sh, the scriptSig will be a valid + // witness program. For nested p2sh, all the bytes after the first data + // push should *exactly* match the witness program template. + if vm.hasFlag(txscript.ScriptVerifyWitness) { + // If witness evaluation is enabled, then P2SH MUST also be + // active. + if !vm.hasFlag(txscript.ScriptBip16) { + errStr := "P2SH must be enabled to do witness verification" + return nil, scriptError(txscript.ErrInvalidFlags, errStr) + } + + var witProgram []byte + + switch { + case txscript.IsWitnessProgram(vm.scripts[1]): + // The scriptSig must be *empty* for all native witness + // programs, otherwise we introduce malleability. + if len(scriptSig) != 0 { + errStr := "native witness program cannot " + + "also have a signature script" + return nil, scriptError(txscript.ErrWitnessMalleated, errStr) + } + + witProgram = scriptPubKey + case len(tx.TxIn[txIdx].Witness) != 0 && vm.bip16: + // The sigScript MUST be *exactly* a single canonical + // data push of the witness program, otherwise we + // reintroduce malleability. + sigPops := vm.scripts[0] + if len(sigPops) > 2 && + isCanonicalPush(sigPops[0], sigPops[1:]) && + txscript.IsWitnessProgram(sigPops[1:]) { + + witProgram = sigPops[1:] + } else { + errStr := "signature script for witness " + + "nested p2sh is not canonical" + return nil, scriptError(txscript.ErrWitnessMalleatedP2SH, errStr) + } + } + + if witProgram != nil { + var err error + vm.witnessVersion, vm.witnessProgram, err = txscript.ExtractWitnessProgramInfo( + witProgram, + ) + if err != nil { + return nil, err + } + } else { + // If we didn't find a witness program in either the + // pkScript or as a datapush within the sigScript, then + // there MUST NOT be any witness data associated with + // the input being validated. + if vm.witnessProgram == nil && len(tx.TxIn[txIdx].Witness) != 0 { + errStr := "non-witness inputs cannot have a witness" + return nil, scriptError(txscript.ErrWitnessUnexpected, errStr) + } + } + + } + + // Setup the current tokenizer used to parse through the script one opcode + // at a time with the script associated with the program counter. + vm.tokenizer = MakeScriptTokenizer(scriptVersion, scripts[vm.scriptIdx]) + + vm.tx = *tx + vm.txIdx = txIdx + + return &vm, nil +} + +// NewEngine returns a new script engine with a script execution callback set. +// This is useful for debugging script execution. +func NewDebugEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, + flags txscript.ScriptFlags, sigCache *txscript.SigCache, hashCache *txscript.TxSigHashes, + inputAmount int64, prevOutFetcher txscript.PrevOutputFetcher, + stepCallback func(*StepInfo) error) (*Engine, error) { + + vm, err := NewEngine( + scriptPubKey, tx, txIdx, flags, sigCache, hashCache, + inputAmount, prevOutFetcher, + ) + if err != nil { + return nil, err + } + + vm.stepCallback = stepCallback + return vm, nil +} + +// isCanonicalPush returns true if the opcode is either not a push instruction +// or the data associated with the push instruction uses the smallest +// instruction to do the job. False otherwise. +// +// For example, it is possible to push a value of 1 to the stack as "OP_1", +// "OP_DATA_1 0x01", "OP_PUSHDATA1 0x01 0x01", and others, however, the first +// only takes a single byte, while the rest take more. Only the first is +// considered canonical. +func isCanonicalPush(opcode byte, data []byte) bool { + dataLen := len(data) + if opcode > OP_16 { + return true + } + + if opcode < OP_PUSHDATA1 && opcode > OP_0 && (dataLen == 1 && data[0] <= 16) { + return false + } + if opcode == OP_PUSHDATA1 && dataLen < OP_PUSHDATA1 { + return false + } + if opcode == OP_PUSHDATA2 && dataLen <= 0xff { + return false + } + if opcode == OP_PUSHDATA4 && dataLen <= 0xffff { + return false + } + return true +} + +// checkScriptParses returns an error if the provided script fails to parse. +func checkScriptParses(scriptVersion uint16, script []byte) error { + tokenizer := MakeScriptTokenizer(scriptVersion, script) + for tokenizer.Next() { + // Nothing to do. + } + return tokenizer.Err() +} + +// isAnnexedWitness returns true if the passed witness has a final push +// that is a witness annex. +func isAnnexedWitness(witness wire.TxWitness) bool { + if len(witness) < 2 { + return false + } + + lastElement := witness[len(witness)-1] + return len(lastElement) > 0 && lastElement[0] == txscript.TaprootAnnexTag +} + +// extractAnnex attempts to extract the annex from the passed witness. If the +// witness doesn't contain an annex, then an error is returned. +func extractAnnex(witness [][]byte) ([]byte, error) { + if !isAnnexedWitness(witness) { + return nil, scriptError(txscript.ErrWitnessHasNoAnnex, "") + } + + lastElement := witness[len(witness)-1] + return lastElement, nil +} + +// removeOpcodeByData will return the script minus any opcodes that perform a +// canonical push of data that contains the passed data to remove. This +// function assumes it is provided a version 0 script as any future version of +// script should avoid this functionality since it is unnecessary due to the +// signature scripts not being part of the witness-free transaction hash. +// +// WARNING: This will return the passed script unmodified unless a modification +// is necessary in which case the modified script is returned. This implies +// callers may NOT rely on being able to safely mutate either the passed or +// returned script without potentially modifying the same data. +// +// NOTE: This function is only valid for version 0 scripts. Since the function +// does not accept a script version, the results are undefined for other script +// versions. +func removeOpcodeByData(script []byte, dataToRemove []byte) ([]byte, bool) { + // Avoid work when possible. + if len(script) == 0 || len(dataToRemove) == 0 { + return script, false + } + + // Parse through the script looking for a canonical data push that contains + // the data to remove. + const scriptVersion = 0 + var result []byte + var prevOffset int32 + var match bool + tokenizer := MakeScriptTokenizer(scriptVersion, script) + for tokenizer.Next() { + var found bool + result, prevOffset, found = removeOpcodeCanonical( + &tokenizer, script, dataToRemove, prevOffset, result, + ) + if found { + match = true + } + } + if result == nil { + result = script + } + return result, match +} + +func removeOpcodeCanonical(t *ScriptTokenizer, script, dataToRemove []byte, + prevOffset int32, result []byte) ([]byte, int32, bool) { + + var found bool + + // In practice, the script will basically never actually contain the + // data since this function is only used during signature verification + // to remove the signature itself which would require some incredibly + // non-standard code to create. + // + // Thus, as an optimization, avoid allocating a new script unless there + // is actually a match that needs to be removed. + op, data := t.Opcode(), t.Data() + if isCanonicalPush(op, data) && bytes.Equal(data, dataToRemove) { + if result == nil { + fullPushLen := t.ByteIndex() - prevOffset + result = make([]byte, 0, int32(len(script))-fullPushLen) + result = append(result, script[0:prevOffset]...) + } + found = true + } else if result != nil { + result = append(result, script[prevOffset:t.ByteIndex()]...) + } + + return result, t.ByteIndex(), found +} diff --git a/pkg/ark-lib/arkade/engine_test.go b/pkg/ark-lib/arkade/engine_test.go new file mode 100644 index 000000000..1d2298581 --- /dev/null +++ b/pkg/ark-lib/arkade/engine_test.go @@ -0,0 +1,1912 @@ +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package arkade + +import ( + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +// TestBadPC sets the pc to a deliberately bad result then confirms that Step +// and Disasm fail correctly. +func TestBadPC(t *testing.T) { + t.Parallel() + + tests := []struct { + scriptIdx int + }{ + {scriptIdx: 2}, + {scriptIdx: 3}, + } + + // tx with almost empty scripts. + tx := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash([32]byte{ + 0xc9, 0x97, 0xa5, 0xe5, + 0x6e, 0x10, 0x41, 0x02, + 0xfa, 0x20, 0x9c, 0x6a, + 0x85, 0x2d, 0xd9, 0x06, + 0x60, 0xa2, 0x0b, 0x2d, + 0x9c, 0x35, 0x24, 0x23, + 0xed, 0xce, 0x25, 0x85, + 0x7f, 0xcd, 0x37, 0x04, + }), + Index: 0, + }, + SignatureScript: mustParseShortForm("NOP"), + Sequence: 4294967295, + }, + }, + TxOut: []*wire.TxOut{{ + Value: 1000000000, + PkScript: nil, + }}, + LockTime: 0, + } + pkScript := mustParseShortForm("NOP") + + for _, test := range tests { + vm, err := NewEngine(pkScript, tx, 0, 0, nil, nil, -1, nil) + if err != nil { + t.Errorf("Failed to create script: %v", err) + } + + // Set to after all scripts. + vm.scriptIdx = test.scriptIdx + + // Ensure attempting to step fails. + _, err = vm.Step() + if err == nil { + t.Errorf("Step with invalid pc (%v) succeeds!", test) + continue + } + + // Ensure attempting to disassemble the current program counter fails. + _, err = vm.DisasmPC() + if err == nil { + t.Errorf("DisasmPC with invalid pc (%v) succeeds!", test) + } + } +} + +// TestCheckErrorCondition tests the execute early test in CheckErrorCondition() +// since most code paths are tested elsewhere. +func TestCheckErrorCondition(t *testing.T) { + t.Parallel() + + // tx with almost empty scripts. + tx := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash([32]byte{ + 0xc9, 0x97, 0xa5, 0xe5, + 0x6e, 0x10, 0x41, 0x02, + 0xfa, 0x20, 0x9c, 0x6a, + 0x85, 0x2d, 0xd9, 0x06, + 0x60, 0xa2, 0x0b, 0x2d, + 0x9c, 0x35, 0x24, 0x23, + 0xed, 0xce, 0x25, 0x85, + 0x7f, 0xcd, 0x37, 0x04, + }), + Index: 0, + }, + SignatureScript: nil, + Sequence: 4294967295, + }}, + TxOut: []*wire.TxOut{{ + Value: 1000000000, + PkScript: nil, + }}, + LockTime: 0, + } + pkScript := mustParseShortForm("NOP NOP NOP NOP NOP NOP NOP NOP NOP" + + " NOP TRUE") + + vm, err := NewEngine(pkScript, tx, 0, 0, nil, nil, 0, nil) + if err != nil { + t.Errorf("failed to create script: %v", err) + } + + for i := 0; i < len(pkScript)-1; i++ { + done, err := vm.Step() + if err != nil { + t.Fatalf("failed to step %dth time: %v", i, err) + } + if done { + t.Fatalf("finished early on %dth time", i) + } + + err = vm.CheckErrorCondition(false) + if !txscript.IsErrorCode(err, txscript.ErrScriptUnfinished) { + t.Fatalf("got unexpected error %v on %dth iteration", + err, i) + } + } + done, err := vm.Step() + if err != nil { + t.Fatalf("final step failed %v", err) + } + if !done { + t.Fatalf("final step isn't done!") + } + + err = vm.CheckErrorCondition(false) + if err != nil { + t.Errorf("unexpected error %v on final check", err) + } +} + +// TestInvalidFlagCombinations ensures the script engine returns the expected +// error when disallowed flag combinations are specified. +func TestInvalidFlagCombinations(t *testing.T) { + t.Parallel() + + tests := []txscript.ScriptFlags{ + txscript.ScriptVerifyCleanStack, + } + + // tx with almost empty scripts. + tx := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash([32]byte{ + 0xc9, 0x97, 0xa5, 0xe5, + 0x6e, 0x10, 0x41, 0x02, + 0xfa, 0x20, 0x9c, 0x6a, + 0x85, 0x2d, 0xd9, 0x06, + 0x60, 0xa2, 0x0b, 0x2d, + 0x9c, 0x35, 0x24, 0x23, + 0xed, 0xce, 0x25, 0x85, + 0x7f, 0xcd, 0x37, 0x04, + }), + Index: 0, + }, + SignatureScript: []uint8{OP_NOP}, + Sequence: 4294967295, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 1000000000, + PkScript: nil, + }, + }, + LockTime: 0, + } + pkScript := []byte{OP_NOP} + + for i, test := range tests { + _, err := NewEngine(pkScript, tx, 0, test, nil, nil, -1, nil) + if !txscript.IsErrorCode(err, txscript.ErrInvalidFlags) { + t.Fatalf("TestInvalidFlagCombinations #%d unexpected "+ + "error: %v", i, err) + } + } +} + +// TestCheckPubKeyEncoding ensures the internal checkPubKeyEncoding function +// works as expected. +func TestCheckPubKeyEncoding(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + key []byte + isValid bool + }{ + { + name: "uncompressed ok", + key: hexToBytes("0411db93e1dcdb8a016b49840f8c53bc1eb68" + + "a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf" + + "9744464f82e160bfa9b8b64f9d4c03f999b8643f656b" + + "412a3"), + isValid: true, + }, + { + name: "compressed ok", + key: hexToBytes("02ce0b14fb842b1ba549fdd675c98075f12e9" + + "c510f8ef52bd021a9a1f4809d3b4d"), + isValid: true, + }, + { + name: "compressed ok", + key: hexToBytes("032689c7c2dab13309fb143e0e8fe39634252" + + "1887e976690b6b47f5b2a4b7d448e"), + isValid: true, + }, + { + name: "hybrid", + key: hexToBytes("0679be667ef9dcbbac55a06295ce870b07029" + + "bfcdb2dce28d959f2815b16f81798483ada7726a3c46" + + "55da4fbfc0e1108a8fd17b448a68554199c47d08ffb1" + + "0d4b8"), + isValid: false, + }, + { + name: "empty", + key: nil, + isValid: false, + }, + } + + vm := Engine{flags: txscript.ScriptVerifyStrictEncoding} + for _, test := range tests { + err := vm.checkPubKeyEncoding(test.key) + if err != nil && test.isValid { + t.Errorf("checkSignatureEncoding test '%s' failed "+ + "when it should have succeeded: %v", test.name, + err) + } else if err == nil && !test.isValid { + t.Errorf("checkSignatureEncooding test '%s' succeeded "+ + "when it should have failed", test.name) + } + } + +} + +// TestCheckSignatureEncoding ensures the internal checkSignatureEncoding +// function works as expected. +func TestCheckSignatureEncoding(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + sig []byte + isValid bool + }{ + { + name: "valid signature", + sig: hexToBytes("304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: true, + }, + { + name: "empty.", + sig: nil, + isValid: false, + }, + { + name: "bad magic", + sig: hexToBytes("314402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "bad 1st int marker magic", + sig: hexToBytes("304403204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "bad 2nd int marker", + sig: hexToBytes("304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41032018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "short len", + sig: hexToBytes("304302204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "long len", + sig: hexToBytes("304502204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "long X", + sig: hexToBytes("304402424e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "long Y", + sig: hexToBytes("304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022118152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "short Y", + sig: hexToBytes("304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41021918152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "trailing crap", + sig: hexToBytes("304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d0901"), + isValid: false, + }, + { + name: "X == N ", + sig: hexToBytes("30440220fffffffffffffffffffffffffffff" + + "ffebaaedce6af48a03bbfd25e8cd0364141022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "X == N ", + sig: hexToBytes("30440220fffffffffffffffffffffffffffff" + + "ffebaaedce6af48a03bbfd25e8cd0364142022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09"), + isValid: false, + }, + { + name: "Y == N", + sig: hexToBytes("304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd410220fffff" + + "ffffffffffffffffffffffffffebaaedce6af48a03bb" + + "fd25e8cd0364141"), + isValid: false, + }, + { + name: "Y > N", + sig: hexToBytes("304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd410220fffff" + + "ffffffffffffffffffffffffffebaaedce6af48a03bb" + + "fd25e8cd0364142"), + isValid: false, + }, + { + name: "0 len X", + sig: hexToBytes("302402000220181522ec8eca07de4860a4acd" + + "d12909d831cc56cbbac4622082221a8768d1d09"), + isValid: false, + }, + { + name: "0 len Y", + sig: hexToBytes("302402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd410200"), + isValid: false, + }, + { + name: "extra R padding", + sig: hexToBytes("30450221004e45e16932b8af514961a1d3a1a" + + "25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181" + + "522ec8eca07de4860a4acdd12909d831cc56cbbac462" + + "2082221a8768d1d09"), + isValid: false, + }, + { + name: "extra S padding", + sig: hexToBytes("304502204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022100181" + + "522ec8eca07de4860a4acdd12909d831cc56cbbac462" + + "2082221a8768d1d09"), + isValid: false, + }, + } + + vm := Engine{flags: txscript.ScriptVerifyStrictEncoding} + for _, test := range tests { + err := vm.checkSignatureEncoding(test.sig) + if err != nil && test.isValid { + t.Errorf("checkSignatureEncoding test '%s' failed "+ + "when it should have succeeded: %v", test.name, + err) + } else if err == nil && !test.isValid { + t.Errorf("checkSignatureEncooding test '%s' succeeded "+ + "when it should have failed", test.name) + } + } +} + +func TestNewOpcodes(t *testing.T) { + t.Parallel() + + type testCase struct { + valid bool + tx *wire.MsgTx + txIdx int + inputAmount int64 + stack [][]byte + } + + type fixture struct { + name string + script *txscript.ScriptBuilder + cases []testCase + } + + prevoutFetcher := txscript.NewMultiPrevOutFetcher(map[wire.OutPoint]*wire.TxOut{ + { + Hash: chainhash.Hash{}, + Index: 0, + }: { + Value: 1000000000, + PkScript: []byte{ + OP_1, OP_DATA_32, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + }) + + tests := []fixture{ + { + name: "OP_MOD", + script: txscript.NewScriptBuilder().AddOp(OP_4).AddOp(OP_3).AddOp(OP_MOD).AddOp(OP_1).AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_DIV", + script: txscript.NewScriptBuilder().AddOp(OP_DIV).AddOp(OP_3).AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: [][]byte{{0x06}, {0x02}}, + }, + { + valid: false, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: [][]byte{ + {0x00}, // Divisor of 0 should fail + {0x01}, + }, + }, + }, + }, + { + name: "OP_MUL", + script: txscript.NewScriptBuilder().AddOp(OP_MUL).AddOp(OP_6).AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: [][]byte{{0x02}, {0x03}}, // 2 * 3 = 6 + }, + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: [][]byte{{0x06}, {0x01}}, // 6 * 1 = 6 + }, + }, + }, + { + name: "OP_XOR", + script: txscript.NewScriptBuilder().AddOp(OP_XOR).AddOp(OP_6).AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: [][]byte{ + {0x05}, // 5 (0101) + {0x03}, // 3 (0011) + // 5 XOR 3 = 6 (0110) + }, + }, + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: [][]byte{ + {0x0F}, // 15 (1111) + {0x09}, // 9 (1001) + // 15 XOR 9 = 6 (0110) + }, + }, + }, + }, + { + name: "OP_CAT", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x01, 0x02}). + AddData([]byte{0x03, 0x04}). + AddOp(OP_CAT). + AddData([]byte{0x01, 0x02, 0x03, 0x04}). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_SUBSTR", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x01, 0x02, 0x03, 0x04}). + AddData([]byte{0x01}). + AddData([]byte{0x02}). + AddOp(OP_SUBSTR). + AddData([]byte{0x02, 0x03}). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_LEFT", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x01, 0x02, 0x03}). + AddData([]byte{0x02}). + AddOp(OP_LEFT). + AddData([]byte{0x01, 0x02}). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_RIGHT", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x01, 0x02, 0x03}). + AddData([]byte{0x02}). + AddOp(OP_RIGHT). + AddData([]byte{0x02, 0x03}). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INVERT", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00, 0xFF}). + AddOp(OP_INVERT). + AddData([]byte{0xFF, 0x00}). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_AND", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x06}). // 0110 + AddData([]byte{0x0C}). // 1100 + AddOp(OP_AND). + AddData([]byte{0x04}). // 0100 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_OR", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x07}). // 0111 + AddData([]byte{0x05}). // 0101 + AddOp(OP_OR). + AddData([]byte{0x07}). // 0111 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_LSHIFT", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x03}). // 0011 + AddData([]byte{0x01}). // Shift by 1 + AddOp(OP_LSHIFT). + AddData([]byte{0x06}). // 0110 (shifted left by 1) + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_RSHIFT", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x06}). // 0110 + AddData([]byte{0x01}). // Shift by 1 + AddOp(OP_RSHIFT). + AddData([]byte{0x03}). // 0011 (shifted right by 1) + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_ADD64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 1 in LE64 + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddOp(OP_ADD64). + AddOp(OP_1). // success flag + AddOp(OP_EQUALVERIFY). + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_ADD64_OVERFLOW", + script: txscript.NewScriptBuilder(). + AddData([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F}). // Max positive int64 + AddData([]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 1 in LE64 + AddOp(OP_ADD64). + AddData([]byte{0x00}). // overflow flag + AddOp(OP_EQUALVERIFY), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_SUB64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddData([]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 1 in LE64 + AddOp(OP_SUB64). + AddOp(OP_1). // success flag + AddOp(OP_EQUALVERIFY). + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_SUB64_OVERFLOW", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}). // Min negative int64 (-9223372036854775808) + AddData([]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 1 in LE64 + AddOp(OP_SUB64). + AddData([]byte{0x00}). // overflow flag + AddOp(OP_EQUALVERIFY), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_MUL64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_MUL64). + AddOp(OP_1). // success flag + AddOp(OP_EQUALVERIFY). + AddData([]byte{0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 6 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_MUL64_OVERFLOW", + script: txscript.NewScriptBuilder(). + AddData([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F}). // Max positive int64 + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_MUL64). + AddData([]byte{0x00}). // overflow flag + AddOp(OP_EQUALVERIFY), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_DIV64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 6 in LE64 + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddOp(OP_DIV64). + AddOp(OP_1). // success flag + AddOp(OP_EQUALVERIFY). + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_NEG64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_NEG64). + AddOp(OP_1). // success flag + AddOp(OP_EQUALVERIFY). + AddData([]byte{0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}). // -3 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_NEG64_OVERFLOW", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}). // Min negative int64 + AddOp(OP_NEG64). + AddData([]byte{0x00}). // overflow flag + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_LESSTHAN64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 1 in LE64 + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddOp(OP_LESSTHAN64). + AddData([]byte{0x01}). // true + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_LESSTHANOREQUAL64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddOp(OP_LESSTHANOREQUAL64). + AddData([]byte{0x01}). // true + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_GREATERTHAN64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddData([]byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 1 in LE64 + AddOp(OP_GREATERTHAN64). + AddData([]byte{0x01}). // true + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_GREATERTHANOREQUAL64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddData([]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 2 in LE64 + AddOp(OP_GREATERTHANOREQUAL64). + AddData([]byte{0x01}). // true + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_SCRIPTNUMTOLE64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x03}). // ScriptNum 3 + AddOp(OP_SCRIPTNUMTOLE64). + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_LE64TOSCRIPTNUM", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_LE64TOSCRIPTNUM). + AddData([]byte{0x03}). // ScriptNum 3 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_LE32TOLE64", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x03, 0x00, 0x00, 0x00}). // 3 in LE32 + AddOp(OP_LE32TOLE64). + AddData([]byte{0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}). // 3 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTINPUTOUTPOINT", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00}). // success flag + AddOp(OP_INSPECTINPUTOUTPOINT). + AddData([]byte{0x00}). // Index + AddOp(OP_EQUALVERIFY). + AddData([]byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }). // Hash + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTINPUTVALUE", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00}). + AddOp(OP_INSPECTINPUTVALUE). + AddData([]byte{0x00, 0xCA, 0x9A, 0x3B, 0x00, 0x00, 0x00, 0x00}). // 1000000000 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 1000000000, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTINPUTSCRIPTPUBKEY", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00}). + AddOp(OP_INSPECTINPUTSCRIPTPUBKEY). + AddOp(OP_1). // segwit v1 + AddOp(OP_EQUALVERIFY). + AddData([]byte{ // witness program + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTINPUTSEQUENCE", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00}). + AddOp(OP_INSPECTINPUTSEQUENCE). + AddData([]byte{0xFF, 0xFF, 0xFF, 0xFF}). // Max sequence number + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + Sequence: 4294967295, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_PUSHCURRENTINPUTINDEX", + script: txscript.NewScriptBuilder(). + AddOp(OP_PUSHCURRENTINPUTINDEX). + AddData([]byte{0x00}). // Input index 0 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTOUTPUTVALUE", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00}). + AddOp(OP_INSPECTOUTPUTVALUE). + AddData([]byte{0x00, 0xCA, 0x9A, 0x3B, 0x00, 0x00, 0x00, 0x00}). // 1000000000 in LE64 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 1000000000, + PkScript: nil, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTOUTPUTSCRIPTPUBKEY", + script: txscript.NewScriptBuilder(). + AddData([]byte{0x00}). + AddOp(OP_INSPECTOUTPUTSCRIPTPUBKEY). + AddOp(OP_1). // Expected scriptPubKey + AddOp(OP_EQUALVERIFY). + AddData([]byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0, + PkScript: []byte{ + OP_1, OP_DATA_32, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTVERSION", + script: txscript.NewScriptBuilder(). + AddOp(OP_INSPECTVERSION). + AddData([]byte{0x01, 0x00, 0x00, 0x00}). // Version 1 in LE32 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTLOCKTIME", + script: txscript.NewScriptBuilder(). + AddOp(OP_INSPECTLOCKTIME). + AddData([]byte{0x00, 0x00, 0x00, 0x00}). // LockTime 0 in LE32 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + LockTime: 0, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTNUMINPUTS", + script: txscript.NewScriptBuilder(). + AddOp(OP_INSPECTNUMINPUTS). + AddOp(OP_1). // 1 input + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_INSPECTNUMOUTPUTS", + script: txscript.NewScriptBuilder(). + AddOp(OP_INSPECTNUMOUTPUTS). + AddData([]byte{0x01}). // 1 output + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0, + PkScript: nil, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_TXWEIGHT", + script: txscript.NewScriptBuilder(). + AddOp(OP_TXWEIGHT). + AddData([]byte{0xCC, 0x00, 0x00, 0x00}). // Expected weight 204 in LE32 + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "OP_CHECKSIGFROMSTACK", + script: txscript.NewScriptBuilder(). + AddData([]byte{ // signature + 0xE9, 0x07, 0x83, 0x1F, 0x80, 0x84, 0x8D, 0x10, + 0x69, 0xA5, 0x37, 0x1B, 0x40, 0x24, 0x10, 0x36, + 0x4B, 0xDF, 0x1C, 0x5F, 0x83, 0x07, 0xB0, 0x08, + 0x4C, 0x55, 0xF1, 0xCE, 0x2D, 0xCA, 0x82, 0x15, + 0x25, 0xF6, 0x6A, 0x4A, 0x85, 0xEA, 0x8B, 0x71, + 0xE4, 0x82, 0xA7, 0x4F, 0x38, 0x2D, 0x2C, 0xE5, + 0xEB, 0xEE, 0xE8, 0xFD, 0xB2, 0x17, 0x2F, 0x47, + 0x7D, 0xF4, 0x90, 0x0D, 0x31, 0x05, 0x36, 0xC0, + }). + AddData([]byte{ // message + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }). + AddData([]byte{ // public key + 0xF9, 0x30, 0x8A, 0x01, 0x92, 0x58, 0xC3, 0x10, + 0x49, 0x34, 0x4F, 0x85, 0xF8, 0x9D, 0x52, 0x29, + 0xB5, 0x31, 0xC8, 0x45, 0x83, 0x6F, 0x99, 0xB0, + 0x86, 0x01, 0xF1, 0x13, 0xBC, 0xE0, 0x36, 0xF9, + }). + AddOp(OP_CHECKSIGFROMSTACK), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + { + name: "SHA256_STREAMING", + script: txscript.NewScriptBuilder(). + AddData([]byte("Hello")). // stack = [Hello] + AddOp(OP_SHA256INITIALIZE). // stack = [shactx(Hello)] + AddData([]byte(" World")). // stack = [shactx(Hello), World] + AddOp(OP_SHA256UPDATE). // stack = [shactx(Hello+World)] + AddData([]byte("!")). // stack = [shactx(Hello+World), !] + AddOp(OP_SHA256FINALIZE). // stack = [sha256(Hello+World+!)] + AddData([]byte{ + 0x7f, 0x83, 0xb1, 0x65, 0x7f, 0xf1, 0xfc, 0x53, + 0xb9, 0x2d, 0xc1, 0x81, 0x48, 0xa1, 0xd6, 0x5d, + 0xfc, 0x2d, 0x4b, 0x1f, 0xa3, 0xd6, 0x77, 0x28, + 0x4a, 0xdd, 0xd2, 0x00, 0x12, 0x6d, 0x90, 0x69, + }). + AddOp(OP_EQUAL), + cases: []testCase{ + { + valid: true, + tx: &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + }, + }, + }, + txIdx: 0, + inputAmount: 0, + stack: nil, + }, + }, + }, + } + + for _, test := range tests { + for caseIndex, c := range test.cases { + t.Run(fmt.Sprintf("%s_%d", test.name, caseIndex), func(tt *testing.T) { + script, err := test.script.Script() + if err != nil { + tt.Errorf("NewEngine failed: %v", err) + } + + engine, err := NewEngine( + script, + c.tx, c.txIdx, + txscript.StandardVerifyFlags&txscript.ScriptVerifyTaproot, + txscript.NewSigCache(100), + txscript.NewTxSigHashes(c.tx, prevoutFetcher), + c.inputAmount, + prevoutFetcher, + ) + if err != nil { + tt.Errorf("NewEngine failed: %v", err) + } + + if len(c.stack) > 0 { + engine.SetStack(c.stack) + } + + err = engine.Execute() + if c.valid && err != nil { + tt.Errorf("Execute failed: %v", err) + } + + if !c.valid && err == nil { + tt.Errorf("Execute should have failed") + } + }) + } + } +} + +// mustParseShortForm parses the passed short form script and returns the +// resulting bytes. It panics if an error occurs. This is only used in the +// tests as a helper since the only way it can fail is if there is an error in +// the test source code. +func mustParseShortForm(script string) []byte { + s, err := parseShortForm(script) + if err != nil { + panic("invalid short form script in test source: err " + + err.Error() + ", script: " + script) + } + + return s +} + +// shortFormOps holds a map of opcode names to values for use in short form +// parsing. It is declared here so it only needs to be created once. +var shortFormOps map[string]byte + +// parseShortForm parses a string as as used in the Bitcoin Core reference tests +// into the script it came from. +// +// The format used for these tests is pretty simple if ad-hoc: +// - Opcodes other than the push opcodes and unknown are present as +// either OP_NAME or just NAME +// - Plain numbers are made into push operations +// - Numbers beginning with 0x are inserted into the []byte as-is (so +// 0x14 is OP_DATA_20) +// - Single quoted strings are pushed as data +// - Anything else is an error +func parseShortForm(script string) ([]byte, error) { + // Only create the short form opcode map once. + if shortFormOps == nil { + ops := make(map[string]byte) + for opcodeName, opcodeValue := range OpcodeByName { + if strings.Contains(opcodeName, "OP_UNKNOWN") { + continue + } + ops[opcodeName] = opcodeValue + + // The opcodes named OP_# can't have the OP_ prefix + // stripped or they would conflict with the plain + // numbers. Also, since OP_FALSE and OP_TRUE are + // aliases for the OP_0, and OP_1, respectively, they + // have the same value, so detect those by name and + // allow them. + if (opcodeName == "OP_FALSE" || opcodeName == "OP_TRUE") || + (opcodeValue != OP_0 && (opcodeValue < OP_1 || + opcodeValue > OP_16)) { + + ops[strings.TrimPrefix(opcodeName, "OP_")] = opcodeValue + } + } + shortFormOps = ops + } + + // Split only does one separator so convert all \n and tab into space. + script = strings.Replace(script, "\n", " ", -1) + script = strings.Replace(script, "\t", " ", -1) + tokens := strings.Split(script, " ") + builder := txscript.NewScriptBuilder() + + for _, tok := range tokens { + if len(tok) == 0 { + continue + } + // if parses as a plain number + if num, err := strconv.ParseInt(tok, 10, 64); err == nil { + builder.AddInt64(num) + continue + } else if _, err := parseHex(tok); err == nil { + // Concatenate the bytes manually since the test code + // intentionally creates scripts that are too large and + // would cause the builder to error otherwise. + _, err := builder.Script() + if err == nil { + return nil, fmt.Errorf("script too large") + } + } else if len(tok) >= 2 && + tok[0] == '\'' && tok[len(tok)-1] == '\'' { + builder.AddFullData([]byte(tok[1 : len(tok)-1])) + } else if opcode, ok := shortFormOps[tok]; ok { + builder.AddOp(opcode) + } else { + return nil, fmt.Errorf("bad token %q", tok) + } + + } + return builder.Script() +} + +// parse hex string into a []byte. +func parseHex(tok string) ([]byte, error) { + if !strings.HasPrefix(tok, "0x") { + return nil, errors.New("not a hex number") + } + return hex.DecodeString(tok[2:]) +} diff --git a/pkg/ark-lib/arkade/error.go b/pkg/ark-lib/arkade/error.go new file mode 100644 index 000000000..4b93d01f3 --- /dev/null +++ b/pkg/ark-lib/arkade/error.go @@ -0,0 +1,8 @@ +package arkade + +import "github.com/btcsuite/btcd/txscript" + +// scriptError creates an Error given a set of arguments. +func scriptError(c txscript.ErrorCode, desc string) error { + return txscript.Error{ErrorCode: c, Description: desc} +} diff --git a/pkg/ark-lib/arkade/opcode.go b/pkg/ark-lib/arkade/opcode.go new file mode 100644 index 000000000..50c33c3aa --- /dev/null +++ b/pkg/ark-lib/arkade/opcode.go @@ -0,0 +1,3512 @@ +package arkade + +import ( + "bytes" + "crypto/sha1" + "crypto/sha256" + "encoding/binary" + "encoding/gob" + "encoding/hex" + "errors" + "fmt" + "hash" + "math" + "math/big" + "strings" + + //nolint:staticcheck + "golang.org/x/crypto/ripemd160" + "modernc.org/mathutil" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +// An opcode defines the information related to a txscript opcode. opfunc, if +// present, is the function to call to perform the opcode on the script. The +// current script is passed in as a slice with the first member being the opcode +// itself. +type opcode struct { + value byte + name string + length int + opfunc func(*opcode, []byte, *Engine) error +} + +// These constants are the values of the official opcodes used on the btc wiki, +// in bitcoin core and in most if not all other references and software related +// to handling BTC scripts. +const ( + OP_0 = 0x00 // 0 + OP_FALSE = 0x00 // 0 - AKA OP_0 + OP_DATA_1 = 0x01 // 1 + OP_DATA_2 = 0x02 // 2 + OP_DATA_3 = 0x03 // 3 + OP_DATA_4 = 0x04 // 4 + OP_DATA_5 = 0x05 // 5 + OP_DATA_6 = 0x06 // 6 + OP_DATA_7 = 0x07 // 7 + OP_DATA_8 = 0x08 // 8 + OP_DATA_9 = 0x09 // 9 + OP_DATA_10 = 0x0a // 10 + OP_DATA_11 = 0x0b // 11 + OP_DATA_12 = 0x0c // 12 + OP_DATA_13 = 0x0d // 13 + OP_DATA_14 = 0x0e // 14 + OP_DATA_15 = 0x0f // 15 + OP_DATA_16 = 0x10 // 16 + OP_DATA_17 = 0x11 // 17 + OP_DATA_18 = 0x12 // 18 + OP_DATA_19 = 0x13 // 19 + OP_DATA_20 = 0x14 // 20 + OP_DATA_21 = 0x15 // 21 + OP_DATA_22 = 0x16 // 22 + OP_DATA_23 = 0x17 // 23 + OP_DATA_24 = 0x18 // 24 + OP_DATA_25 = 0x19 // 25 + OP_DATA_26 = 0x1a // 26 + OP_DATA_27 = 0x1b // 27 + OP_DATA_28 = 0x1c // 28 + OP_DATA_29 = 0x1d // 29 + OP_DATA_30 = 0x1e // 30 + OP_DATA_31 = 0x1f // 31 + OP_DATA_32 = 0x20 // 32 + OP_DATA_33 = 0x21 // 33 + OP_DATA_34 = 0x22 // 34 + OP_DATA_35 = 0x23 // 35 + OP_DATA_36 = 0x24 // 36 + OP_DATA_37 = 0x25 // 37 + OP_DATA_38 = 0x26 // 38 + OP_DATA_39 = 0x27 // 39 + OP_DATA_40 = 0x28 // 40 + OP_DATA_41 = 0x29 // 41 + OP_DATA_42 = 0x2a // 42 + OP_DATA_43 = 0x2b // 43 + OP_DATA_44 = 0x2c // 44 + OP_DATA_45 = 0x2d // 45 + OP_DATA_46 = 0x2e // 46 + OP_DATA_47 = 0x2f // 47 + OP_DATA_48 = 0x30 // 48 + OP_DATA_49 = 0x31 // 49 + OP_DATA_50 = 0x32 // 50 + OP_DATA_51 = 0x33 // 51 + OP_DATA_52 = 0x34 // 52 + OP_DATA_53 = 0x35 // 53 + OP_DATA_54 = 0x36 // 54 + OP_DATA_55 = 0x37 // 55 + OP_DATA_56 = 0x38 // 56 + OP_DATA_57 = 0x39 // 57 + OP_DATA_58 = 0x3a // 58 + OP_DATA_59 = 0x3b // 59 + OP_DATA_60 = 0x3c // 60 + OP_DATA_61 = 0x3d // 61 + OP_DATA_62 = 0x3e // 62 + OP_DATA_63 = 0x3f // 63 + OP_DATA_64 = 0x40 // 64 + OP_DATA_65 = 0x41 // 65 + OP_DATA_66 = 0x42 // 66 + OP_DATA_67 = 0x43 // 67 + OP_DATA_68 = 0x44 // 68 + OP_DATA_69 = 0x45 // 69 + OP_DATA_70 = 0x46 // 70 + OP_DATA_71 = 0x47 // 71 + OP_DATA_72 = 0x48 // 72 + OP_DATA_73 = 0x49 // 73 + OP_DATA_74 = 0x4a // 74 + OP_DATA_75 = 0x4b // 75 + OP_PUSHDATA1 = 0x4c // 76 + OP_PUSHDATA2 = 0x4d // 77 + OP_PUSHDATA4 = 0x4e // 78 + OP_1NEGATE = 0x4f // 79 + OP_RESERVED = 0x50 // 80 + OP_1 = 0x51 // 81 - AKA OP_TRUE + OP_TRUE = 0x51 // 81 + OP_2 = 0x52 // 82 + OP_3 = 0x53 // 83 + OP_4 = 0x54 // 84 + OP_5 = 0x55 // 85 + OP_6 = 0x56 // 86 + OP_7 = 0x57 // 87 + OP_8 = 0x58 // 88 + OP_9 = 0x59 // 89 + OP_10 = 0x5a // 90 + OP_11 = 0x5b // 91 + OP_12 = 0x5c // 92 + OP_13 = 0x5d // 93 + OP_14 = 0x5e // 94 + OP_15 = 0x5f // 95 + OP_16 = 0x60 // 96 + OP_NOP = 0x61 // 97 + OP_VER = 0x62 // 98 + OP_IF = 0x63 // 99 + OP_NOTIF = 0x64 // 100 + OP_VERIF = 0x65 // 101 + OP_VERNOTIF = 0x66 // 102 + OP_ELSE = 0x67 // 103 + OP_ENDIF = 0x68 // 104 + OP_VERIFY = 0x69 // 105 + OP_RETURN = 0x6a // 106 + OP_TOALTSTACK = 0x6b // 107 + OP_FROMALTSTACK = 0x6c // 108 + OP_2DROP = 0x6d // 109 + OP_2DUP = 0x6e // 110 + OP_3DUP = 0x6f // 111 + OP_2OVER = 0x70 // 112 + OP_2ROT = 0x71 // 113 + OP_2SWAP = 0x72 // 114 + OP_IFDUP = 0x73 // 115 + OP_DEPTH = 0x74 // 116 + OP_DROP = 0x75 // 117 + OP_DUP = 0x76 // 118 + OP_NIP = 0x77 // 119 + OP_OVER = 0x78 // 120 + OP_PICK = 0x79 // 121 + OP_ROLL = 0x7a // 122 + OP_ROT = 0x7b // 123 + OP_SWAP = 0x7c // 124 + OP_TUCK = 0x7d // 125 + OP_CAT = 0x7e // 126 + OP_SUBSTR = 0x7f // 127 + OP_LEFT = 0x80 // 128 + OP_RIGHT = 0x81 // 129 + OP_SIZE = 0x82 // 130 + OP_INVERT = 0x83 // 131 + OP_AND = 0x84 // 132 + OP_OR = 0x85 // 133 + OP_XOR = 0x86 // 134 + OP_EQUAL = 0x87 // 135 + OP_EQUALVERIFY = 0x88 // 136 + OP_RESERVED1 = 0x89 // 137 + OP_RESERVED2 = 0x8a // 138 + OP_1ADD = 0x8b // 139 + OP_1SUB = 0x8c // 140 + OP_2MUL = 0x8d // 141 + OP_2DIV = 0x8e // 142 + OP_NEGATE = 0x8f // 143 + OP_ABS = 0x90 // 144 + OP_NOT = 0x91 // 145 + OP_0NOTEQUAL = 0x92 // 146 + OP_ADD = 0x93 // 147 + OP_SUB = 0x94 // 148 + OP_MUL = 0x95 // 149 + OP_DIV = 0x96 // 150 + OP_MOD = 0x97 // 151 + OP_LSHIFT = 0x98 // 152 + OP_RSHIFT = 0x99 // 153 + OP_BOOLAND = 0x9a // 154 + OP_BOOLOR = 0x9b // 155 + OP_NUMEQUAL = 0x9c // 156 + OP_NUMEQUALVERIFY = 0x9d // 157 + OP_NUMNOTEQUAL = 0x9e // 158 + OP_LESSTHAN = 0x9f // 159 + OP_GREATERTHAN = 0xa0 // 160 + OP_LESSTHANOREQUAL = 0xa1 // 161 + OP_GREATERTHANOREQUAL = 0xa2 // 162 + OP_MIN = 0xa3 // 163 + OP_MAX = 0xa4 // 164 + OP_WITHIN = 0xa5 // 165 + OP_RIPEMD160 = 0xa6 // 166 + OP_SHA1 = 0xa7 // 167 + OP_SHA256 = 0xa8 // 168 + OP_HASH160 = 0xa9 // 169 + OP_HASH256 = 0xaa // 170 + OP_CODESEPARATOR = 0xab // 171 + OP_CHECKSIG = 0xac // 172 + OP_CHECKSIGVERIFY = 0xad // 173 + OP_CHECKMULTISIG = 0xae // 174 + OP_CHECKMULTISIGVERIFY = 0xaf // 175 + OP_NOP1 = 0xb0 // 176 + OP_NOP2 = 0xb1 // 177 + OP_CHECKLOCKTIMEVERIFY = 0xb1 // 177 - AKA OP_NOP2 + OP_NOP3 = 0xb2 // 178 + OP_CHECKSEQUENCEVERIFY = 0xb2 // 178 - AKA OP_NOP3 + OP_NOP4 = 0xb3 // 179 + OP_NOP5 = 0xb4 // 180 + OP_NOP6 = 0xb5 // 181 + OP_NOP7 = 0xb6 // 182 + OP_NOP8 = 0xb7 // 183 + OP_NOP9 = 0xb8 // 184 + OP_NOP10 = 0xb9 // 185 + OP_CHECKSIGADD = 0xba // 186 + OP_UNKNOWN187 = 0xbb // 187 + OP_UNKNOWN188 = 0xbc // 188 + OP_UNKNOWN189 = 0xbd // 189 + OP_UNKNOWN190 = 0xbe // 190 + OP_UNKNOWN191 = 0xbf // 191 + OP_UNKNOWN192 = 0xc0 // 192 + OP_UNKNOWN193 = 0xc1 // 193 + OP_UNKNOWN194 = 0xc2 // 194 + OP_UNKNOWN195 = 0xc3 // 195 + OP_SHA256INITIALIZE = 0xc4 // 196 + OP_SHA256UPDATE = 0xc5 // 197 + OP_SHA256FINALIZE = 0xc6 // 198 + + // Inputs + OP_INSPECTINPUTOUTPOINT = 0xc7 // 199 + + OP_UNKNOWN200 = 0xc8 // 200 + + OP_INSPECTINPUTVALUE = 0xc9 // 201 + OP_INSPECTINPUTSCRIPTPUBKEY = 0xca // 202 + OP_INSPECTINPUTSEQUENCE = 0xcb // 203 + + OP_CHECKSIGFROMSTACK = 0xcc // 204 + + // current index + OP_PUSHCURRENTINPUTINDEX = 0xcd // 205 + + OP_UNKNOWN206 = 0xce // 206 + + // outputs + OP_INSPECTOUTPUTVALUE = 0xcf // 207 + + OP_UNKNOWN208 = 0xd0 // 208 + + OP_INSPECTOUTPUTSCRIPTPUBKEY = 0xd1 // 209 + + // transaction + OP_INSPECTVERSION = 0xd2 // 210 + OP_INSPECTLOCKTIME = 0xd3 // 211 + OP_INSPECTNUMINPUTS = 0xd4 // 212 + OP_INSPECTNUMOUTPUTS = 0xd5 // 213 + OP_TXWEIGHT = 0xd6 // 214 + + OP_ADD64 = 0xd7 + OP_SUB64 = 0xd8 + OP_MUL64 = 0xd9 + OP_DIV64 = 0xda + OP_NEG64 = 0xdb + OP_LESSTHAN64 = 0xdc + OP_LESSTHANOREQUAL64 = 0xdd + OP_GREATERTHAN64 = 0xde + OP_GREATERTHANOREQUAL64 = 0xdf // 223 + OP_SCRIPTNUMTOLE64 = 0xe0 // 224 + OP_LE64TOSCRIPTNUM = 0xe1 // 225 + OP_LE32TOLE64 = 0xe2 // 226 + OP_ECMULSCALARVERIFY = 0xe3 // 227 + OP_TWEAKVERIFY = 0xe4 // 228 + OP_UNKNOWN229 = 0xe5 // 229 + OP_UNKNOWN230 = 0xe6 // 230 + OP_UNKNOWN231 = 0xe7 // 231 + OP_UNKNOWN232 = 0xe8 // 232 + OP_UNKNOWN233 = 0xe9 // 233 + OP_UNKNOWN234 = 0xea // 234 + OP_UNKNOWN235 = 0xeb // 235 + OP_UNKNOWN236 = 0xec // 236 + OP_UNKNOWN237 = 0xed // 237 + OP_UNKNOWN238 = 0xee // 238 + OP_UNKNOWN239 = 0xef // 239 + OP_UNKNOWN240 = 0xf0 // 240 + OP_UNKNOWN241 = 0xf1 // 241 + OP_UNKNOWN242 = 0xf2 // 242 + OP_UNKNOWN243 = 0xf3 // 243 + OP_UNKNOWN244 = 0xf4 // 244 + OP_UNKNOWN245 = 0xf5 // 245 + OP_UNKNOWN246 = 0xf6 // 246 + OP_UNKNOWN247 = 0xf7 // 247 + OP_UNKNOWN248 = 0xf8 // 248 + OP_UNKNOWN249 = 0xf9 // 249 + OP_SMALLINTEGER = 0xfa // 250 - bitcoin core internal + OP_PUBKEYS = 0xfb // 251 - bitcoin core internal + OP_UNKNOWN252 = 0xfc // 252 + OP_PUBKEYHASH = 0xfd // 253 - bitcoin core internal + OP_PUBKEY = 0xfe // 254 - bitcoin core internal + OP_INVALIDOPCODE = 0xff // 255 - bitcoin core internal +) + +// Conditional execution constants. +const ( + OpCondFalse = 0 + OpCondTrue = 1 + OpCondSkip = 2 +) + +// opcodeArray holds details about all possible opcodes such as how many bytes +// the opcode and any associated data should take, its human-readable name, and +// the handler function. +var opcodeArray = [256]opcode{ + // Data push opcodes. + OP_FALSE: {OP_FALSE, "OP_0", 1, opcodeFalse}, + OP_DATA_1: {OP_DATA_1, "OP_DATA_1", 2, opcodePushData}, + OP_DATA_2: {OP_DATA_2, "OP_DATA_2", 3, opcodePushData}, + OP_DATA_3: {OP_DATA_3, "OP_DATA_3", 4, opcodePushData}, + OP_DATA_4: {OP_DATA_4, "OP_DATA_4", 5, opcodePushData}, + OP_DATA_5: {OP_DATA_5, "OP_DATA_5", 6, opcodePushData}, + OP_DATA_6: {OP_DATA_6, "OP_DATA_6", 7, opcodePushData}, + OP_DATA_7: {OP_DATA_7, "OP_DATA_7", 8, opcodePushData}, + OP_DATA_8: {OP_DATA_8, "OP_DATA_8", 9, opcodePushData}, + OP_DATA_9: {OP_DATA_9, "OP_DATA_9", 10, opcodePushData}, + OP_DATA_10: {OP_DATA_10, "OP_DATA_10", 11, opcodePushData}, + OP_DATA_11: {OP_DATA_11, "OP_DATA_11", 12, opcodePushData}, + OP_DATA_12: {OP_DATA_12, "OP_DATA_12", 13, opcodePushData}, + OP_DATA_13: {OP_DATA_13, "OP_DATA_13", 14, opcodePushData}, + OP_DATA_14: {OP_DATA_14, "OP_DATA_14", 15, opcodePushData}, + OP_DATA_15: {OP_DATA_15, "OP_DATA_15", 16, opcodePushData}, + OP_DATA_16: {OP_DATA_16, "OP_DATA_16", 17, opcodePushData}, + OP_DATA_17: {OP_DATA_17, "OP_DATA_17", 18, opcodePushData}, + OP_DATA_18: {OP_DATA_18, "OP_DATA_18", 19, opcodePushData}, + OP_DATA_19: {OP_DATA_19, "OP_DATA_19", 20, opcodePushData}, + OP_DATA_20: {OP_DATA_20, "OP_DATA_20", 21, opcodePushData}, + OP_DATA_21: {OP_DATA_21, "OP_DATA_21", 22, opcodePushData}, + OP_DATA_22: {OP_DATA_22, "OP_DATA_22", 23, opcodePushData}, + OP_DATA_23: {OP_DATA_23, "OP_DATA_23", 24, opcodePushData}, + OP_DATA_24: {OP_DATA_24, "OP_DATA_24", 25, opcodePushData}, + OP_DATA_25: {OP_DATA_25, "OP_DATA_25", 26, opcodePushData}, + OP_DATA_26: {OP_DATA_26, "OP_DATA_26", 27, opcodePushData}, + OP_DATA_27: {OP_DATA_27, "OP_DATA_27", 28, opcodePushData}, + OP_DATA_28: {OP_DATA_28, "OP_DATA_28", 29, opcodePushData}, + OP_DATA_29: {OP_DATA_29, "OP_DATA_29", 30, opcodePushData}, + OP_DATA_30: {OP_DATA_30, "OP_DATA_30", 31, opcodePushData}, + OP_DATA_31: {OP_DATA_31, "OP_DATA_31", 32, opcodePushData}, + OP_DATA_32: {OP_DATA_32, "OP_DATA_32", 33, opcodePushData}, + OP_DATA_33: {OP_DATA_33, "OP_DATA_33", 34, opcodePushData}, + OP_DATA_34: {OP_DATA_34, "OP_DATA_34", 35, opcodePushData}, + OP_DATA_35: {OP_DATA_35, "OP_DATA_35", 36, opcodePushData}, + OP_DATA_36: {OP_DATA_36, "OP_DATA_36", 37, opcodePushData}, + OP_DATA_37: {OP_DATA_37, "OP_DATA_37", 38, opcodePushData}, + OP_DATA_38: {OP_DATA_38, "OP_DATA_38", 39, opcodePushData}, + OP_DATA_39: {OP_DATA_39, "OP_DATA_39", 40, opcodePushData}, + OP_DATA_40: {OP_DATA_40, "OP_DATA_40", 41, opcodePushData}, + OP_DATA_41: {OP_DATA_41, "OP_DATA_41", 42, opcodePushData}, + OP_DATA_42: {OP_DATA_42, "OP_DATA_42", 43, opcodePushData}, + OP_DATA_43: {OP_DATA_43, "OP_DATA_43", 44, opcodePushData}, + OP_DATA_44: {OP_DATA_44, "OP_DATA_44", 45, opcodePushData}, + OP_DATA_45: {OP_DATA_45, "OP_DATA_45", 46, opcodePushData}, + OP_DATA_46: {OP_DATA_46, "OP_DATA_46", 47, opcodePushData}, + OP_DATA_47: {OP_DATA_47, "OP_DATA_47", 48, opcodePushData}, + OP_DATA_48: {OP_DATA_48, "OP_DATA_48", 49, opcodePushData}, + OP_DATA_49: {OP_DATA_49, "OP_DATA_49", 50, opcodePushData}, + OP_DATA_50: {OP_DATA_50, "OP_DATA_50", 51, opcodePushData}, + OP_DATA_51: {OP_DATA_51, "OP_DATA_51", 52, opcodePushData}, + OP_DATA_52: {OP_DATA_52, "OP_DATA_52", 53, opcodePushData}, + OP_DATA_53: {OP_DATA_53, "OP_DATA_53", 54, opcodePushData}, + OP_DATA_54: {OP_DATA_54, "OP_DATA_54", 55, opcodePushData}, + OP_DATA_55: {OP_DATA_55, "OP_DATA_55", 56, opcodePushData}, + OP_DATA_56: {OP_DATA_56, "OP_DATA_56", 57, opcodePushData}, + OP_DATA_57: {OP_DATA_57, "OP_DATA_57", 58, opcodePushData}, + OP_DATA_58: {OP_DATA_58, "OP_DATA_58", 59, opcodePushData}, + OP_DATA_59: {OP_DATA_59, "OP_DATA_59", 60, opcodePushData}, + OP_DATA_60: {OP_DATA_60, "OP_DATA_60", 61, opcodePushData}, + OP_DATA_61: {OP_DATA_61, "OP_DATA_61", 62, opcodePushData}, + OP_DATA_62: {OP_DATA_62, "OP_DATA_62", 63, opcodePushData}, + OP_DATA_63: {OP_DATA_63, "OP_DATA_63", 64, opcodePushData}, + OP_DATA_64: {OP_DATA_64, "OP_DATA_64", 65, opcodePushData}, + OP_DATA_65: {OP_DATA_65, "OP_DATA_65", 66, opcodePushData}, + OP_DATA_66: {OP_DATA_66, "OP_DATA_66", 67, opcodePushData}, + OP_DATA_67: {OP_DATA_67, "OP_DATA_67", 68, opcodePushData}, + OP_DATA_68: {OP_DATA_68, "OP_DATA_68", 69, opcodePushData}, + OP_DATA_69: {OP_DATA_69, "OP_DATA_69", 70, opcodePushData}, + OP_DATA_70: {OP_DATA_70, "OP_DATA_70", 71, opcodePushData}, + OP_DATA_71: {OP_DATA_71, "OP_DATA_71", 72, opcodePushData}, + OP_DATA_72: {OP_DATA_72, "OP_DATA_72", 73, opcodePushData}, + OP_DATA_73: {OP_DATA_73, "OP_DATA_73", 74, opcodePushData}, + OP_DATA_74: {OP_DATA_74, "OP_DATA_74", 75, opcodePushData}, + OP_DATA_75: {OP_DATA_75, "OP_DATA_75", 76, opcodePushData}, + OP_PUSHDATA1: {OP_PUSHDATA1, "OP_PUSHDATA1", -1, opcodePushData}, + OP_PUSHDATA2: {OP_PUSHDATA2, "OP_PUSHDATA2", -2, opcodePushData}, + OP_PUSHDATA4: {OP_PUSHDATA4, "OP_PUSHDATA4", -4, opcodePushData}, + OP_1NEGATE: {OP_1NEGATE, "OP_1NEGATE", 1, opcode1Negate}, + OP_RESERVED: {OP_RESERVED, "OP_RESERVED", 1, opcodeReserved}, + OP_TRUE: {OP_TRUE, "OP_1", 1, opcodeN}, + OP_2: {OP_2, "OP_2", 1, opcodeN}, + OP_3: {OP_3, "OP_3", 1, opcodeN}, + OP_4: {OP_4, "OP_4", 1, opcodeN}, + OP_5: {OP_5, "OP_5", 1, opcodeN}, + OP_6: {OP_6, "OP_6", 1, opcodeN}, + OP_7: {OP_7, "OP_7", 1, opcodeN}, + OP_8: {OP_8, "OP_8", 1, opcodeN}, + OP_9: {OP_9, "OP_9", 1, opcodeN}, + OP_10: {OP_10, "OP_10", 1, opcodeN}, + OP_11: {OP_11, "OP_11", 1, opcodeN}, + OP_12: {OP_12, "OP_12", 1, opcodeN}, + OP_13: {OP_13, "OP_13", 1, opcodeN}, + OP_14: {OP_14, "OP_14", 1, opcodeN}, + OP_15: {OP_15, "OP_15", 1, opcodeN}, + OP_16: {OP_16, "OP_16", 1, opcodeN}, + + // Control opcodes. + OP_NOP: {OP_NOP, "OP_NOP", 1, opcodeNop}, + OP_VER: {OP_VER, "OP_VER", 1, opcodeReserved}, + OP_IF: {OP_IF, "OP_IF", 1, opcodeIf}, + OP_NOTIF: {OP_NOTIF, "OP_NOTIF", 1, opcodeNotIf}, + OP_VERIF: {OP_VERIF, "OP_VERIF", 1, opcodeReserved}, + OP_VERNOTIF: {OP_VERNOTIF, "OP_VERNOTIF", 1, opcodeReserved}, + OP_ELSE: {OP_ELSE, "OP_ELSE", 1, opcodeElse}, + OP_ENDIF: {OP_ENDIF, "OP_ENDIF", 1, opcodeEndif}, + OP_VERIFY: {OP_VERIFY, "OP_VERIFY", 1, opcodeVerify}, + OP_RETURN: {OP_RETURN, "OP_RETURN", 1, opcodeReturn}, + OP_CHECKLOCKTIMEVERIFY: {OP_CHECKLOCKTIMEVERIFY, "OP_CHECKLOCKTIMEVERIFY", 1, opcodeCheckLockTimeVerify}, + OP_CHECKSEQUENCEVERIFY: {OP_CHECKSEQUENCEVERIFY, "OP_CHECKSEQUENCEVERIFY", 1, opcodeCheckSequenceVerify}, + + // Stack opcodes. + OP_TOALTSTACK: {OP_TOALTSTACK, "OP_TOALTSTACK", 1, opcodeToAltStack}, + OP_FROMALTSTACK: {OP_FROMALTSTACK, "OP_FROMALTSTACK", 1, opcodeFromAltStack}, + OP_2DROP: {OP_2DROP, "OP_2DROP", 1, opcode2Drop}, + OP_2DUP: {OP_2DUP, "OP_2DUP", 1, opcode2Dup}, + OP_3DUP: {OP_3DUP, "OP_3DUP", 1, opcode3Dup}, + OP_2OVER: {OP_2OVER, "OP_2OVER", 1, opcode2Over}, + OP_2ROT: {OP_2ROT, "OP_2ROT", 1, opcode2Rot}, + OP_2SWAP: {OP_2SWAP, "OP_2SWAP", 1, opcode2Swap}, + OP_IFDUP: {OP_IFDUP, "OP_IFDUP", 1, opcodeIfDup}, + OP_DEPTH: {OP_DEPTH, "OP_DEPTH", 1, opcodeDepth}, + OP_DROP: {OP_DROP, "OP_DROP", 1, opcodeDrop}, + OP_DUP: {OP_DUP, "OP_DUP", 1, opcodeDup}, + OP_NIP: {OP_NIP, "OP_NIP", 1, opcodeNip}, + OP_OVER: {OP_OVER, "OP_OVER", 1, opcodeOver}, + OP_PICK: {OP_PICK, "OP_PICK", 1, opcodePick}, + OP_ROLL: {OP_ROLL, "OP_ROLL", 1, opcodeRoll}, + OP_ROT: {OP_ROT, "OP_ROT", 1, opcodeRot}, + OP_SWAP: {OP_SWAP, "OP_SWAP", 1, opcodeSwap}, + OP_TUCK: {OP_TUCK, "OP_TUCK", 1, opcodeTuck}, + + // Splice opcodes. + OP_CAT: {OP_CAT, "OP_CAT", 1, opcodeCat}, + OP_SUBSTR: {OP_SUBSTR, "OP_SUBSTR", 1, opcodeSubstr}, + OP_LEFT: {OP_LEFT, "OP_LEFT", 1, opcodeLeft}, + OP_RIGHT: {OP_RIGHT, "OP_RIGHT", 1, opcodeRight}, + OP_SIZE: {OP_SIZE, "OP_SIZE", 1, opcodeSize}, + + // Bitwise logic opcodes. + OP_INVERT: {OP_INVERT, "OP_INVERT", 1, opcodeInvert}, + OP_AND: {OP_AND, "OP_AND", 1, opcodeAnd}, + OP_OR: {OP_OR, "OP_OR", 1, opcodeOr}, + OP_XOR: {OP_XOR, "OP_XOR", 1, opcodeXor}, + OP_EQUAL: {OP_EQUAL, "OP_EQUAL", 1, opcodeEqual}, + OP_EQUALVERIFY: {OP_EQUALVERIFY, "OP_EQUALVERIFY", 1, opcodeEqualVerify}, + OP_RESERVED1: {OP_RESERVED1, "OP_RESERVED1", 1, opcodeReserved}, + OP_RESERVED2: {OP_RESERVED2, "OP_RESERVED2", 1, opcodeReserved}, + + // Numeric related opcodes. + OP_1ADD: {OP_1ADD, "OP_1ADD", 1, opcode1Add}, + OP_1SUB: {OP_1SUB, "OP_1SUB", 1, opcode1Sub}, + OP_2MUL: {OP_2MUL, "OP_2MUL", 1, opcode2Mul}, + OP_2DIV: {OP_2DIV, "OP_2DIV", 1, opcode2Div}, + OP_NEGATE: {OP_NEGATE, "OP_NEGATE", 1, opcodeNegate}, + OP_ABS: {OP_ABS, "OP_ABS", 1, opcodeAbs}, + OP_NOT: {OP_NOT, "OP_NOT", 1, opcodeNot}, + OP_0NOTEQUAL: {OP_0NOTEQUAL, "OP_0NOTEQUAL", 1, opcode0NotEqual}, + OP_ADD: {OP_ADD, "OP_ADD", 1, opcodeAdd}, + OP_SUB: {OP_SUB, "OP_SUB", 1, opcodeSub}, + OP_MUL: {OP_MUL, "OP_MUL", 1, opcodeMul}, + OP_DIV: {OP_DIV, "OP_DIV", 1, opcodeDiv}, + OP_MOD: {OP_MOD, "OP_MOD", 1, opcodeMod}, + OP_LSHIFT: {OP_LSHIFT, "OP_LSHIFT", 1, opcodeLshift}, + OP_RSHIFT: {OP_RSHIFT, "OP_RSHIFT", 1, opcodeRshift}, + OP_BOOLAND: {OP_BOOLAND, "OP_BOOLAND", 1, opcodeBoolAnd}, + OP_BOOLOR: {OP_BOOLOR, "OP_BOOLOR", 1, opcodeBoolOr}, + OP_NUMEQUAL: {OP_NUMEQUAL, "OP_NUMEQUAL", 1, opcodeNumEqual}, + OP_NUMEQUALVERIFY: {OP_NUMEQUALVERIFY, "OP_NUMEQUALVERIFY", 1, opcodeNumEqualVerify}, + OP_NUMNOTEQUAL: {OP_NUMNOTEQUAL, "OP_NUMNOTEQUAL", 1, opcodeNumNotEqual}, + OP_LESSTHAN: {OP_LESSTHAN, "OP_LESSTHAN", 1, opcodeLessThan}, + OP_GREATERTHAN: {OP_GREATERTHAN, "OP_GREATERTHAN", 1, opcodeGreaterThan}, + OP_LESSTHANOREQUAL: {OP_LESSTHANOREQUAL, "OP_LESSTHANOREQUAL", 1, opcodeLessThanOrEqual}, + OP_GREATERTHANOREQUAL: {OP_GREATERTHANOREQUAL, "OP_GREATERTHANOREQUAL", 1, opcodeGreaterThanOrEqual}, + OP_MIN: {OP_MIN, "OP_MIN", 1, opcodeMin}, + OP_MAX: {OP_MAX, "OP_MAX", 1, opcodeMax}, + OP_WITHIN: {OP_WITHIN, "OP_WITHIN", 1, opcodeWithin}, + + // Crypto opcodes. + OP_RIPEMD160: {OP_RIPEMD160, "OP_RIPEMD160", 1, opcodeRipemd160}, + OP_SHA1: {OP_SHA1, "OP_SHA1", 1, opcodeSha1}, + OP_SHA256: {OP_SHA256, "OP_SHA256", 1, opcodeSha256}, + OP_HASH160: {OP_HASH160, "OP_HASH160", 1, opcodeHash160}, + OP_HASH256: {OP_HASH256, "OP_HASH256", 1, opcodeHash256}, + OP_CODESEPARATOR: {OP_CODESEPARATOR, "OP_CODESEPARATOR", 1, opcodeCodeSeparator}, + OP_CHECKSIG: {OP_CHECKSIG, "OP_CHECKSIG", 1, opcodeCheckSig}, + OP_CHECKSIGVERIFY: {OP_CHECKSIGVERIFY, "OP_CHECKSIGVERIFY", 1, opcodeCheckSigVerify}, + OP_CHECKMULTISIG: {OP_CHECKMULTISIG, "OP_CHECKMULTISIG", 1, opcodeCheckMultiSig}, + OP_CHECKMULTISIGVERIFY: {OP_CHECKMULTISIGVERIFY, "OP_CHECKMULTISIGVERIFY", 1, opcodeCheckMultiSigVerify}, + OP_CHECKSIGADD: {OP_CHECKSIGADD, "OP_CHECKSIGADD", 1, opcodeCheckSigAdd}, + + // Reserved opcodes. + OP_NOP1: {OP_NOP1, "OP_NOP1", 1, opcodeNop}, + OP_NOP4: {OP_NOP4, "OP_NOP4", 1, opcodeNop}, + OP_NOP5: {OP_NOP5, "OP_NOP5", 1, opcodeNop}, + OP_NOP6: {OP_NOP6, "OP_NOP6", 1, opcodeNop}, + OP_NOP7: {OP_NOP7, "OP_NOP7", 1, opcodeNop}, + OP_NOP8: {OP_NOP8, "OP_NOP8", 1, opcodeNop}, + OP_NOP9: {OP_NOP9, "OP_NOP9", 1, opcodeNop}, + OP_NOP10: {OP_NOP10, "OP_NOP10", 1, opcodeNop}, + + // Undefined opcodes. + OP_UNKNOWN187: {OP_UNKNOWN187, "OP_UNKNOWN187", 1, opcodeInvalid}, + OP_UNKNOWN188: {OP_UNKNOWN188, "OP_UNKNOWN188", 1, opcodeInvalid}, + OP_UNKNOWN189: {OP_UNKNOWN189, "OP_UNKNOWN189", 1, opcodeInvalid}, + OP_UNKNOWN190: {OP_UNKNOWN190, "OP_UNKNOWN190", 1, opcodeInvalid}, + OP_UNKNOWN191: {OP_UNKNOWN191, "OP_UNKNOWN191", 1, opcodeInvalid}, + OP_UNKNOWN192: {OP_UNKNOWN192, "OP_UNKNOWN192", 1, opcodeInvalid}, + OP_UNKNOWN193: {OP_UNKNOWN193, "OP_UNKNOWN193", 1, opcodeInvalid}, + OP_UNKNOWN194: {OP_UNKNOWN194, "OP_UNKNOWN194", 1, opcodeInvalid}, + OP_UNKNOWN195: {OP_UNKNOWN195, "OP_UNKNOWN195", 1, opcodeInvalid}, + // Streaming opcodes + OP_SHA256INITIALIZE: {OP_SHA256INITIALIZE, "OP_SHA256INITIALIZE", 1, opcodeSha256Initialize}, + OP_SHA256UPDATE: {OP_SHA256UPDATE, "OP_SHA256UPDATE", 1, opcodeSha256Update}, + OP_SHA256FINALIZE: {OP_SHA256FINALIZE, "OP_SHA256FINALIZE", 1, opcodeSha256Finalize}, + + // Inputs introspection + OP_INSPECTINPUTOUTPOINT: {OP_INSPECTINPUTOUTPOINT, "OP_INSPECTINPUTOUTPOINT", 1, opcodeInspectInputOutpoint}, + + OP_UNKNOWN200: {OP_UNKNOWN200, "OP_UNKNOWN200", 1, opcodeInvalid}, + + OP_INSPECTINPUTVALUE: {OP_INSPECTINPUTVALUE, "OP_INSPECTINPUTVALUE", 1, opcodeInspectInputValue}, + OP_INSPECTINPUTSCRIPTPUBKEY: {OP_INSPECTINPUTSCRIPTPUBKEY, "OP_INSPECTINPUTSCRIPTPUBKEY", 1, opcodeInspectInputScriptPubkey}, + OP_INSPECTINPUTSEQUENCE: {OP_INSPECTINPUTSEQUENCE, "OP_INSPECTINPUTSEQUENCE", 1, opcodeInspectInputSequence}, + + OP_CHECKSIGFROMSTACK: {OP_CHECKSIGFROMSTACK, "OP_CHECKSIGFROMSTACK", 1, opcodeChecksigFromStack}, + + OP_PUSHCURRENTINPUTINDEX: {OP_PUSHCURRENTINPUTINDEX, "OP_PUSHCURRENTINPUTINDEX", 1, opcodePushCurrentInputIndex}, + + // Outputs introspection + OP_UNKNOWN206: {OP_UNKNOWN206, "OP_UNKNOWN206", 1, opcodeInvalid}, + + OP_INSPECTOUTPUTVALUE: {OP_INSPECTOUTPUTVALUE, "OP_INSPECTOUTPUTVALUE", 1, opcodeInspectOutputValue}, + + OP_UNKNOWN208: {OP_UNKNOWN208, "OP_UNKNOWN208", 1, opcodeInvalid}, + + OP_INSPECTOUTPUTSCRIPTPUBKEY: {OP_INSPECTOUTPUTSCRIPTPUBKEY, "OP_INSPECTOUTPUTSCRIPTPUBKEY", 1, opcodeInspectOutputScriptPubkey}, + + // Transaction introspection + OP_INSPECTVERSION: {OP_INSPECTVERSION, "OP_INSPECTVERSION", 1, opcodeInspectVersion}, + OP_INSPECTLOCKTIME: {OP_INSPECTLOCKTIME, "OP_INSPECTLOCKTIME", 1, opcodeInspectLocktime}, + OP_INSPECTNUMINPUTS: {OP_INSPECTNUMINPUTS, "OP_INSPECTNUMINPUTS", 1, opcodeInspectNumInputs}, + OP_INSPECTNUMOUTPUTS: {OP_INSPECTNUMOUTPUTS, "OP_INSPECTNUMOUTPUTS", 1, opcodeInspectNumOutputs}, + OP_TXWEIGHT: {OP_TXWEIGHT, "OP_TXWEIGHT", 1, opcodeTxWeight}, + + OP_ADD64: {OP_ADD64, "OP_ADD64", 1, opcodeAdd64}, + OP_SUB64: {OP_SUB64, "OP_SUB64", 1, opcodeSub64}, + OP_MUL64: {OP_MUL64, "OP_MUL64", 1, opcodeMul64}, + OP_DIV64: {OP_DIV64, "OP_DIV64", 1, opcodeDiv64}, + OP_NEG64: {OP_NEG64, "OP_NEG64", 1, opcodeNeg64}, + OP_LESSTHAN64: {OP_LESSTHAN64, "OP_LESSTHAN64", 1, opcodeLessThan64}, + OP_LESSTHANOREQUAL64: {OP_LESSTHANOREQUAL64, "OP_LESSTHANOREQUAL64", 1, opcodeLessThanOrEqual64}, + OP_GREATERTHAN64: {OP_GREATERTHAN64, "OP_GREATERTHAN64", 1, opcodeGreaterThan64}, + OP_GREATERTHANOREQUAL64: {OP_GREATERTHANOREQUAL64, "OP_GREATERTHANOREQUAL64", 1, opcodeGreaterThanOrEqual64}, + OP_SCRIPTNUMTOLE64: {OP_SCRIPTNUMTOLE64, "OP_SCRIPTNUMTOLE64", 1, opcodeScriptNumToLE64}, + OP_LE64TOSCRIPTNUM: {OP_LE64TOSCRIPTNUM, "OP_LE64TOSCRIPTNUM", 1, opcodeLE64ToScriptNum}, + OP_LE32TOLE64: {OP_LE32TOLE64, "OP_LE32TOLE64", 1, opcodeLE32ToLE64}, + OP_ECMULSCALARVERIFY: {OP_ECMULSCALARVERIFY, "OP_ECMULSCALARVERIFY", 1, opcodeECMulScalarVerify}, + OP_TWEAKVERIFY: {OP_TWEAKVERIFY, "OP_TWEAKVERIFY", 1, opcodeTweakVerify}, + OP_UNKNOWN229: {OP_UNKNOWN229, "OP_UNKNOWN229", 1, opcodeInvalid}, + OP_UNKNOWN230: {OP_UNKNOWN230, "OP_UNKNOWN230", 1, opcodeInvalid}, + OP_UNKNOWN231: {OP_UNKNOWN231, "OP_UNKNOWN231", 1, opcodeInvalid}, + OP_UNKNOWN232: {OP_UNKNOWN232, "OP_UNKNOWN232", 1, opcodeInvalid}, + OP_UNKNOWN233: {OP_UNKNOWN233, "OP_UNKNOWN233", 1, opcodeInvalid}, + OP_UNKNOWN234: {OP_UNKNOWN234, "OP_UNKNOWN234", 1, opcodeInvalid}, + OP_UNKNOWN235: {OP_UNKNOWN235, "OP_UNKNOWN235", 1, opcodeInvalid}, + OP_UNKNOWN236: {OP_UNKNOWN236, "OP_UNKNOWN236", 1, opcodeInvalid}, + OP_UNKNOWN237: {OP_UNKNOWN237, "OP_UNKNOWN237", 1, opcodeInvalid}, + OP_UNKNOWN238: {OP_UNKNOWN238, "OP_UNKNOWN238", 1, opcodeInvalid}, + OP_UNKNOWN239: {OP_UNKNOWN239, "OP_UNKNOWN239", 1, opcodeInvalid}, + OP_UNKNOWN240: {OP_UNKNOWN240, "OP_UNKNOWN240", 1, opcodeInvalid}, + OP_UNKNOWN241: {OP_UNKNOWN241, "OP_UNKNOWN241", 1, opcodeInvalid}, + OP_UNKNOWN242: {OP_UNKNOWN242, "OP_UNKNOWN242", 1, opcodeInvalid}, + OP_UNKNOWN243: {OP_UNKNOWN243, "OP_UNKNOWN243", 1, opcodeInvalid}, + OP_UNKNOWN244: {OP_UNKNOWN244, "OP_UNKNOWN244", 1, opcodeInvalid}, + OP_UNKNOWN245: {OP_UNKNOWN245, "OP_UNKNOWN245", 1, opcodeInvalid}, + OP_UNKNOWN246: {OP_UNKNOWN246, "OP_UNKNOWN246", 1, opcodeInvalid}, + OP_UNKNOWN247: {OP_UNKNOWN247, "OP_UNKNOWN247", 1, opcodeInvalid}, + OP_UNKNOWN248: {OP_UNKNOWN248, "OP_UNKNOWN248", 1, opcodeInvalid}, + OP_UNKNOWN249: {OP_UNKNOWN249, "OP_UNKNOWN249", 1, opcodeInvalid}, + + // Bitcoin Core internal use opcode. Defined here for completeness. + OP_SMALLINTEGER: {OP_SMALLINTEGER, "OP_SMALLINTEGER", 1, opcodeInvalid}, + OP_PUBKEYS: {OP_PUBKEYS, "OP_PUBKEYS", 1, opcodeInvalid}, + OP_UNKNOWN252: {OP_UNKNOWN252, "OP_UNKNOWN252", 1, opcodeInvalid}, + OP_PUBKEYHASH: {OP_PUBKEYHASH, "OP_PUBKEYHASH", 1, opcodeInvalid}, + OP_PUBKEY: {OP_PUBKEY, "OP_PUBKEY", 1, opcodeInvalid}, + + OP_INVALIDOPCODE: {OP_INVALIDOPCODE, "OP_INVALIDOPCODE", 1, opcodeInvalid}, +} + +// opcodeOnelineRepls defines opcode names which are replaced when doing a +// one-line disassembly. This is done to match the output of the reference +// implementation while not changing the opcode names in the nicer full +// disassembly. +var opcodeOnelineRepls = map[string]string{ + "OP_1NEGATE": "-1", + "OP_0": "0", + "OP_1": "1", + "OP_2": "2", + "OP_3": "3", + "OP_4": "4", + "OP_5": "5", + "OP_6": "6", + "OP_7": "7", + "OP_8": "8", + "OP_9": "9", + "OP_10": "10", + "OP_11": "11", + "OP_12": "12", + "OP_13": "13", + "OP_14": "14", + "OP_15": "15", + "OP_16": "16", +} + +// disasmOpcode writes a human-readable disassembly of the provided opcode and +// data into the provided buffer. The compact flag indicates the disassembly +// should print a more compact representation of data-carrying and small integer +// opcodes. For example, OP_0 through OP_16 are replaced with the numeric value +// and data pushes are printed as only the hex representation of the data as +// opposed to including the opcode that specifies the amount of data to push as +// well. +func disasmOpcode(buf *strings.Builder, op *opcode, data []byte, compact bool) { + // Replace opcode which represent values (e.g. OP_0 through OP_16 and + // OP_1NEGATE) with the raw value when performing a compact disassembly. + opcodeName := op.name + if compact { + if replName, ok := opcodeOnelineRepls[opcodeName]; ok { + opcodeName = replName + } + + // Either write the human-readable opcode or the parsed data in hex for + // data-carrying opcodes. + switch { + case op.length == 1: + buf.WriteString(opcodeName) + + default: + buf.WriteString(hex.EncodeToString(data)) + } + + return + } + + buf.WriteString(opcodeName) + + switch op.length { + // Only write the opcode name for non-data push opcodes. + case 1: + return + + // Add length for the OP_PUSHDATA# opcodes. + case -1: + buf.WriteString(fmt.Sprintf(" 0x%02x", len(data))) + case -2: + buf.WriteString(fmt.Sprintf(" 0x%04x", len(data))) + case -4: + buf.WriteString(fmt.Sprintf(" 0x%08x", len(data))) + } + + buf.WriteString(fmt.Sprintf(" 0x%02x", data)) +} + +// ******************************************* +// Opcode implementation functions start here. +// ******************************************* + +// opcodeReserved is a common handler for all reserved opcodes. It returns an +// appropriate error indicating the opcode is reserved. +func opcodeReserved(op *opcode, data []byte, vm *Engine) error { + str := fmt.Sprintf("attempt to execute reserved opcode %s", op.name) + return scriptError(txscript.ErrReservedOpcode, str) +} + +// opcodeInvalid is a common handler for all invalid opcodes. It returns an +// appropriate error indicating the opcode is invalid. +func opcodeInvalid(op *opcode, data []byte, vm *Engine) error { + str := fmt.Sprintf("attempt to execute invalid opcode %s", op.name) + return scriptError(txscript.ErrReservedOpcode, str) +} + +// opcodeFalse pushes an empty array to the data stack to represent false. Note +// that 0, when encoded as a number according to the numeric encoding consensus +// rules, is an empty array. +func opcodeFalse(op *opcode, data []byte, vm *Engine) error { + vm.dstack.PushByteArray(nil) + return nil +} + +// opcodePushData is a common handler for the vast majority of opcodes that push +// raw data (bytes) to the data stack. +func opcodePushData(op *opcode, data []byte, vm *Engine) error { + vm.dstack.PushByteArray(data) + return nil +} + +// opcode1Negate pushes -1, encoded as a number, to the data stack. +func opcode1Negate(op *opcode, data []byte, vm *Engine) error { + vm.dstack.PushInt(scriptNum(-1)) + return nil +} + +// opcodeN is a common handler for the small integer data push opcodes. It +// pushes the numeric value the opcode represents (which will be from 1 to 16) +// onto the data stack. +func opcodeN(op *opcode, data []byte, vm *Engine) error { + // The opcodes are all defined consecutively, so the numeric value is + // the difference. + vm.dstack.PushInt(scriptNum((op.value - (OP_1 - 1)))) + return nil +} + +// opcodeNop is a common handler for the NOP family of opcodes. As the name +// implies it generally does nothing, however, it will return an error when +// the flag to discourage use of NOPs is set for select opcodes. +func opcodeNop(op *opcode, data []byte, vm *Engine) error { + switch op.value { + case OP_NOP1, OP_NOP4, OP_NOP5, + OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10: + + if vm.hasFlag(txscript.ScriptDiscourageUpgradableNops) { + str := fmt.Sprintf("%v reserved for soft-fork "+ + "upgrades", op.name) + return scriptError(txscript.ErrDiscourageUpgradableNOPs, str) + } + } + return nil +} + +// popIfBool enforces the "minimal if" policy during script execution if the +// particular flag is set. If so, in order to eliminate an additional source +// of nuisance malleability, post-segwit for version 0 witness programs, we now +// require the following: for OP_IF and OP_NOT_IF, the top stack item MUST +// either be an empty byte slice, or [0x01]. Otherwise, the item at the top of +// the stack will be popped and interpreted as a boolean. +func popIfBool(vm *Engine) (bool, error) { + // When not in witness execution mode, not executing a v0 witness + // program, or not doing tapscript execution, or the minimal if flag + // isn't set pop the top stack item as a normal bool. + switch { + // Minimal if is always on for taproot execution. + case vm.isWitnessVersionActive(txscript.TaprootWitnessVersion): + break + + // If this isn't the base segwit version, then we'll coerce the stack + // element as a bool as normal. + case !vm.isWitnessVersionActive(txscript.BaseSegwitWitnessVersion): + fallthrough + + // If the minimal if flag isn't set, then we don't need any extra + // checks here. + case !vm.hasFlag(txscript.ScriptVerifyMinimalIf): + return vm.dstack.PopBool() + } + + // At this point, a v0 or v1 witness program is being executed and the + // minimal if flag is set, so enforce additional constraints on the top + // stack item. + so, err := vm.dstack.PopByteArray() + if err != nil { + return false, err + } + + // The top element MUST have a length of at least one. + if len(so) > 1 { + str := fmt.Sprintf("minimal if is active, top element MUST "+ + "have a length of at least, instead length is %v", + len(so)) + return false, scriptError(txscript.ErrMinimalIf, str) + } + + // Additionally, if the length is one, then the value MUST be 0x01. + if len(so) == 1 && so[0] != 0x01 { + str := fmt.Sprintf("minimal if is active, top stack item MUST "+ + "be an empty byte array or 0x01, is instead: %v", + so[0]) + return false, scriptError(txscript.ErrMinimalIf, str) + } + + return asBool(so), nil +} + +// opcodeIf treats the top item on the data stack as a boolean and removes it. +// +// An appropriate entry is added to the conditional stack depending on whether +// the boolean is true and whether this if is on an executing branch in order +// to allow proper execution of further opcodes depending on the conditional +// logic. When the boolean is true, the first branch will be executed (unless +// this opcode is nested in a non-executed branch). +// +// if [statements] [else [statements]] endif +// +// Note that, unlike for all non-conditional opcodes, this is executed even when +// it is on a non-executing branch so proper nesting is maintained. +// +// Data stack transformation: [... bool] -> [...] +// Conditional stack transformation: [...] -> [... OpCondValue] +func opcodeIf(op *opcode, data []byte, vm *Engine) error { + condVal := OpCondFalse + if vm.isBranchExecuting() { + ok, err := popIfBool(vm) + if err != nil { + return err + } + + if ok { + condVal = OpCondTrue + } + } else { + condVal = OpCondSkip + } + vm.condStack = append(vm.condStack, condVal) + return nil +} + +// opcodeNotIf treats the top item on the data stack as a boolean and removes +// it. +// +// An appropriate entry is added to the conditional stack depending on whether +// the boolean is true and whether this if is on an executing branch in order +// to allow proper execution of further opcodes depending on the conditional +// logic. When the boolean is false, the first branch will be executed (unless +// this opcode is nested in a non-executed branch). +// +// notif [statements] [else [statements]] endif +// +// Note that, unlike for all non-conditional opcodes, this is executed even when +// it is on a non-executing branch so proper nesting is maintained. +// +// Data stack transformation: [... bool] -> [...] +// Conditional stack transformation: [...] -> [... OpCondValue] +func opcodeNotIf(op *opcode, data []byte, vm *Engine) error { + condVal := OpCondFalse + if vm.isBranchExecuting() { + ok, err := popIfBool(vm) + if err != nil { + return err + } + + if !ok { + condVal = OpCondTrue + } + } else { + condVal = OpCondSkip + } + vm.condStack = append(vm.condStack, condVal) + return nil +} + +// opcodeElse inverts conditional execution for other half of if/else/endif. +// +// An error is returned if there has not already been a matching OP_IF. +// +// Conditional stack transformation: [... OpCondValue] -> [... !OpCondValue] +func opcodeElse(op *opcode, data []byte, vm *Engine) error { + if len(vm.condStack) == 0 { + str := fmt.Sprintf("encountered opcode %s with no matching "+ + "opcode to begin conditional execution", op.name) + return scriptError(txscript.ErrUnbalancedConditional, str) + } + + conditionalIdx := len(vm.condStack) - 1 + switch vm.condStack[conditionalIdx] { + case OpCondTrue: + vm.condStack[conditionalIdx] = OpCondFalse + case OpCondFalse: + vm.condStack[conditionalIdx] = OpCondTrue + case OpCondSkip: + // Value doesn't change in skip since it indicates this opcode + // is nested in a non-executed branch. + } + return nil +} + +// opcodeEndif terminates a conditional block, removing the value from the +// conditional execution stack. +// +// An error is returned if there has not already been a matching OP_IF. +// +// Conditional stack transformation: [... OpCondValue] -> [...] +func opcodeEndif(op *opcode, data []byte, vm *Engine) error { + if len(vm.condStack) == 0 { + str := fmt.Sprintf("encountered opcode %s with no matching "+ + "opcode to begin conditional execution", op.name) + return scriptError(txscript.ErrUnbalancedConditional, str) + } + + vm.condStack = vm.condStack[:len(vm.condStack)-1] + return nil +} + +// abstractVerify examines the top item on the data stack as a boolean value and +// verifies it evaluates to true. An error is returned either when there is no +// item on the stack or when that item evaluates to false. In the latter case +// where the verification fails specifically due to the top item evaluating +// to false, the returned error will use the passed error code. +func abstractVerify(op *opcode, vm *Engine, c txscript.ErrorCode) error { + verified, err := vm.dstack.PopBool() + if err != nil { + return err + } + + if !verified { + str := fmt.Sprintf("%s failed", op.name) + return scriptError(c, str) + } + return nil +} + +// opcodeVerify examines the top item on the data stack as a boolean value and +// verifies it evaluates to true. An error is returned if it does not. +func opcodeVerify(op *opcode, data []byte, vm *Engine) error { + return abstractVerify(op, vm, txscript.ErrVerify) +} + +// opcodeReturn returns an appropriate error since it is always an error to +// return early from a script. +func opcodeReturn(op *opcode, data []byte, vm *Engine) error { + return scriptError(txscript.ErrEarlyReturn, "script returned early") +} + +// verifyLockTime is a helper function used to validate locktimes. +func verifyLockTime(txLockTime, threshold, lockTime int64) error { + // The lockTimes in both the script and transaction must be of the same + // type. + if !((txLockTime < threshold && lockTime < threshold) || + (txLockTime >= threshold && lockTime >= threshold)) { + str := fmt.Sprintf("mismatched locktime types -- tx locktime "+ + "%d, stack locktime %d", txLockTime, lockTime) + return scriptError(txscript.ErrUnsatisfiedLockTime, str) + } + + if lockTime > txLockTime { + str := fmt.Sprintf("locktime requirement not satisfied -- "+ + "locktime is greater than the transaction locktime: "+ + "%d > %d", lockTime, txLockTime) + return scriptError(txscript.ErrUnsatisfiedLockTime, str) + } + + return nil +} + +// opcodeCheckLockTimeVerify compares the top item on the data stack to the +// LockTime field of the transaction containing the script signature +// validating if the transaction outputs are spendable yet. If flag +// ScriptVerifyCheckLockTimeVerify is not set, the code continues as if OP_NOP2 +// were executed. +func opcodeCheckLockTimeVerify(op *opcode, data []byte, vm *Engine) error { + // If the ScriptVerifyCheckLockTimeVerify script flag is not set, treat + // opcode as OP_NOP2 instead. + if !vm.hasFlag(txscript.ScriptVerifyCheckLockTimeVerify) { + if vm.hasFlag(txscript.ScriptDiscourageUpgradableNops) { + return scriptError(txscript.ErrDiscourageUpgradableNOPs, + "OP_NOP2 reserved for soft-fork upgrades") + } + return nil + } + + // The current transaction locktime is a uint32 resulting in a maximum + // locktime of 2^32-1 (the year 2106). However, scriptNums are signed + // and therefore a standard 4-byte scriptNum would only support up to a + // maximum of 2^31-1 (the year 2038). Thus, a 5-byte scriptNum is used + // here since it will support up to 2^39-1 which allows dates beyond the + // current locktime limit. + // + // PeekByteArray is used here instead of PeekInt because we do not want + // to be limited to a 4-byte integer for reasons specified above. + so, err := vm.dstack.PeekByteArray(0) + if err != nil { + return err + } + lockTime, err := MakeScriptNum(so, vm.dstack.verifyMinimalData, 5) + if err != nil { + return err + } + + // In the rare event that the argument needs to be < 0 due to some + // arithmetic being done first, you can always use + // 0 OP_MAX OP_CHECKLOCKTIMEVERIFY. + if lockTime < 0 { + str := fmt.Sprintf("negative lock time: %d", lockTime) + return scriptError(txscript.ErrNegativeLockTime, str) + } + + // The lock time field of a transaction is either a block height at + // which the transaction is finalized or a timestamp depending on if the + // value is before the txscript.LockTimeThreshold. When it is under the + // threshold it is a block height. + err = verifyLockTime(int64(vm.tx.LockTime), txscript.LockTimeThreshold, + int64(lockTime)) + if err != nil { + return err + } + + // The lock time feature can also be disabled, thereby bypassing + // OP_CHECKLOCKTIMEVERIFY, if every transaction input has been finalized by + // setting its sequence to the maximum value (wire.MaxTxInSequenceNum). This + // condition would result in the transaction being allowed into the blockchain + // making the opcode ineffective. + // + // This condition is prevented by enforcing that the input being used by + // the opcode is unlocked (its sequence number is less than the max + // value). This is sufficient to prove correctness without having to + // check every input. + // + // NOTE: This implies that even if the transaction is not finalized due to + // another input being unlocked, the opcode execution will still fail when the + // input being used by the opcode is locked. + if vm.tx.TxIn[vm.txIdx].Sequence == wire.MaxTxInSequenceNum { + return scriptError(txscript.ErrUnsatisfiedLockTime, + "transaction input is finalized") + } + + return nil +} + +// opcodeCheckSequenceVerify compares the top item on the data stack to the +// LockTime field of the transaction containing the script signature +// validating if the transaction outputs are spendable yet. If flag +// ScriptVerifyCheckSequenceVerify is not set, the code continues as if OP_NOP3 +// were executed. +func opcodeCheckSequenceVerify(op *opcode, data []byte, vm *Engine) error { + // If the ScriptVerifyCheckSequenceVerify script flag is not set, treat + // opcode as OP_NOP3 instead. + if !vm.hasFlag(txscript.ScriptVerifyCheckSequenceVerify) { + if vm.hasFlag(txscript.ScriptDiscourageUpgradableNops) { + return scriptError(txscript.ErrDiscourageUpgradableNOPs, + "OP_NOP3 reserved for soft-fork upgrades") + } + return nil + } + + // The current transaction sequence is a uint32 resulting in a maximum + // sequence of 2^32-1. However, scriptNums are signed and therefore a + // standard 4-byte scriptNum would only support up to a maximum of + // 2^31-1. Thus, a 5-byte scriptNum is used here since it will support + // up to 2^39-1 which allows sequences beyond the current sequence + // limit. + // + // PeekByteArray is used here instead of PeekInt because we do not want + // to be limited to a 4-byte integer for reasons specified above. + so, err := vm.dstack.PeekByteArray(0) + if err != nil { + return err + } + stackSequence, err := MakeScriptNum(so, vm.dstack.verifyMinimalData, 5) + if err != nil { + return err + } + + // In the rare event that the argument needs to be < 0 due to some + // arithmetic being done first, you can always use + // 0 OP_MAX OP_CHECKSEQUENCEVERIFY. + if stackSequence < 0 { + str := fmt.Sprintf("negative sequence: %d", stackSequence) + return scriptError(txscript.ErrNegativeLockTime, str) + } + + sequence := int64(stackSequence) + + // To provide for future soft-fork extensibility, if the + // operand has the disabled lock-time flag set, + // CHECKSEQUENCEVERIFY behaves as a NOP. + if sequence&int64(wire.SequenceLockTimeDisabled) != 0 { + return nil + } + + // Transaction version numbers not high enough to trigger CSV rules must + // fail. + if uint32(vm.tx.Version) < 2 { + str := fmt.Sprintf("invalid transaction version: %d", + vm.tx.Version) + return scriptError(txscript.ErrUnsatisfiedLockTime, str) + } + + // Sequence numbers with their most significant bit set are not + // consensus constrained. Testing that the transaction's sequence + // number does not have this bit set prevents using this property + // to get around a CHECKSEQUENCEVERIFY check. + txSequence := int64(vm.tx.TxIn[vm.txIdx].Sequence) + if txSequence&int64(wire.SequenceLockTimeDisabled) != 0 { + str := fmt.Sprintf("transaction sequence has sequence "+ + "locktime disabled bit set: 0x%x", txSequence) + return scriptError(txscript.ErrUnsatisfiedLockTime, str) + } + + // Mask off non-consensus bits before doing comparisons. + lockTimeMask := int64(wire.SequenceLockTimeIsSeconds | + wire.SequenceLockTimeMask) + return verifyLockTime(txSequence&lockTimeMask, + wire.SequenceLockTimeIsSeconds, sequence&lockTimeMask) +} + +// opcodeToAltStack removes the top item from the main data stack and pushes it +// onto the alternate data stack. +// +// Main data stack transformation: [... x1 x2 x3] -> [... x1 x2] +// Alt data stack transformation: [... y1 y2 y3] -> [... y1 y2 y3 x3] +func opcodeToAltStack(op *opcode, data []byte, vm *Engine) error { + so, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + vm.astack.PushByteArray(so) + + return nil +} + +// opcodeFromAltStack removes the top item from the alternate data stack and +// pushes it onto the main data stack. +// +// Main data stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 y3] +// Alt data stack transformation: [... y1 y2 y3] -> [... y1 y2] +func opcodeFromAltStack(op *opcode, data []byte, vm *Engine) error { + so, err := vm.astack.PopByteArray() + if err != nil { + return err + } + vm.dstack.PushByteArray(so) + + return nil +} + +// opcode2Drop removes the top 2 items from the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1] +func opcode2Drop(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.DropN(2) +} + +// opcode2Dup duplicates the top 2 items on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x2 x3] +func opcode2Dup(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.DupN(2) +} + +// opcode3Dup duplicates the top 3 items on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x1 x2 x3] +func opcode3Dup(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.DupN(3) +} + +// opcode2Over duplicates the 2 items before the top 2 items on the data stack. +// +// Stack transformation: [... x1 x2 x3 x4] -> [... x1 x2 x3 x4 x1 x2] +func opcode2Over(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.OverN(2) +} + +// opcode2Rot rotates the top 6 items on the data stack to the left twice. +// +// Stack transformation: [... x1 x2 x3 x4 x5 x6] -> [... x3 x4 x5 x6 x1 x2] +func opcode2Rot(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.RotN(2) +} + +// opcode2Swap swaps the top 2 items on the data stack with the 2 that come +// before them. +// +// Stack transformation: [... x1 x2 x3 x4] -> [... x3 x4 x1 x2] +func opcode2Swap(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.SwapN(2) +} + +// opcodeIfDup duplicates the top item of the stack if it is not zero. +// +// Stack transformation (x1==0): [... x1] -> [... x1] +// Stack transformation (x1!=0): [... x1] -> [... x1 x1] +func opcodeIfDup(op *opcode, data []byte, vm *Engine) error { + so, err := vm.dstack.PeekByteArray(0) + if err != nil { + return err + } + + // Push copy of data iff it isn't zero + if asBool(so) { + vm.dstack.PushByteArray(so) + } + + return nil +} + +// opcodeDepth pushes the depth of the data stack prior to executing this +// opcode, encoded as a number, onto the data stack. +// +// Stack transformation: [...] -> [... ] +// Example with 2 items: [x1 x2] -> [x1 x2 2] +// Example with 3 items: [x1 x2 x3] -> [x1 x2 x3 3] +func opcodeDepth(op *opcode, data []byte, vm *Engine) error { + vm.dstack.PushInt(scriptNum(vm.dstack.Depth())) + return nil +} + +// opcodeDrop removes the top item from the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func opcodeDrop(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.DropN(1) +} + +// opcodeDup duplicates the top item on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x3] +func opcodeDup(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.DupN(1) +} + +// opcodeNip removes the item before the top item on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x3] +func opcodeNip(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.NipN(1) +} + +// opcodeOver duplicates the item before the top item on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x2] +func opcodeOver(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.OverN(1) +} + +// opcodePick treats the top item on the data stack as an integer and duplicates +// the item on the stack that number of items back to the top. +// +// Stack transformation: [xn ... x2 x1 x0 n] -> [xn ... x2 x1 x0 xn] +// Example with n=1: [x2 x1 x0 1] -> [x2 x1 x0 x1] +// Example with n=2: [x2 x1 x0 2] -> [x2 x1 x0 x2] +func opcodePick(op *opcode, data []byte, vm *Engine) error { + val, err := vm.dstack.PopInt() + if err != nil { + return err + } + + return vm.dstack.PickN(val.Int32()) +} + +// opcodeRoll treats the top item on the data stack as an integer and moves +// the item on the stack that number of items back to the top. +// +// Stack transformation: [xn ... x2 x1 x0 n] -> [... x2 x1 x0 xn] +// Example with n=1: [x2 x1 x0 1] -> [x2 x0 x1] +// Example with n=2: [x2 x1 x0 2] -> [x1 x0 x2] +func opcodeRoll(op *opcode, data []byte, vm *Engine) error { + val, err := vm.dstack.PopInt() + if err != nil { + return err + } + + return vm.dstack.RollN(val.Int32()) +} + +// opcodeRot rotates the top 3 items on the data stack to the left. +// +// Stack transformation: [... x1 x2 x3] -> [... x2 x3 x1] +func opcodeRot(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.RotN(1) +} + +// opcodeSwap swaps the top two items on the stack. +// +// Stack transformation: [... x1 x2] -> [... x2 x1] +func opcodeSwap(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.SwapN(1) +} + +// opcodeTuck inserts a duplicate of the top item of the data stack before the +// second-to-top item. +// +// Stack transformation: [... x1 x2] -> [... x2 x1 x2] +func opcodeTuck(op *opcode, data []byte, vm *Engine) error { + return vm.dstack.Tuck() +} + +// opcodeSize pushes the size of the top item of the data stack onto the data +// stack. +// +// Stack transformation: [... x1] -> [... x1 len(x1)] +func opcodeSize(op *opcode, data []byte, vm *Engine) error { + so, err := vm.dstack.PeekByteArray(0) + if err != nil { + return err + } + + vm.dstack.PushInt(scriptNum(len(so))) + return nil +} + +// opcodeEqual removes the top 2 items of the data stack, compares them as raw +// bytes, and pushes the result, encoded as a boolean, back to the stack. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeEqual(op *opcode, data []byte, vm *Engine) error { + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + vm.dstack.PushBool(bytes.Equal(a, b)) + return nil +} + +// opcodeEqualVerify is a combination of opcodeEqual and opcodeVerify. +// Specifically, it removes the top 2 items of the data stack, compares them, +// and pushes the result, encoded as a boolean, back to the stack. Then, it +// examines the top item on the data stack as a boolean value and verifies it +// evaluates to true. An error is returned if it does not. +// +// Stack transformation: [... x1 x2] -> [... bool] -> [...] +func opcodeEqualVerify(op *opcode, data []byte, vm *Engine) error { + err := opcodeEqual(op, data, vm) + if err == nil { + err = abstractVerify(op, vm, txscript.ErrEqualVerify) + } + return err +} + +// opcode1Add treats the top item on the data stack as an integer and replaces +// it with its incremented value (plus 1). +// +// Stack transformation: [... x1 x2] -> [... x1 x2+1] +func opcode1Add(op *opcode, data []byte, vm *Engine) error { + m, err := vm.dstack.PopInt() + if err != nil { + return err + } + + vm.dstack.PushInt(m + 1) + return nil +} + +// opcode1Sub treats the top item on the data stack as an integer and replaces +// it with its decremented value (minus 1). +// +// Stack transformation: [... x1 x2] -> [... x1 x2-1] +func opcode1Sub(op *opcode, data []byte, vm *Engine) error { + m, err := vm.dstack.PopInt() + if err != nil { + return err + } + vm.dstack.PushInt(m - 1) + + return nil +} + +// opcodeNegate treats the top item on the data stack as an integer and replaces +// it with its negation. +// +// Stack transformation: [... x1 x2] -> [... x1 -x2] +func opcodeNegate(op *opcode, data []byte, vm *Engine) error { + m, err := vm.dstack.PopInt() + if err != nil { + return err + } + + vm.dstack.PushInt(-m) + return nil +} + +// opcodeAbs treats the top item on the data stack as an integer and replaces it +// it with its absolute value. +// +// Stack transformation: [... x1 x2] -> [... x1 abs(x2)] +func opcodeAbs(op *opcode, data []byte, vm *Engine) error { + m, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if m < 0 { + m = -m + } + vm.dstack.PushInt(m) + return nil +} + +// opcodeNot treats the top item on the data stack as an integer and replaces +// it with its "inverted" value (0 becomes 1, non-zero becomes 0). +// +// NOTE: While it would probably make more sense to treat the top item as a +// boolean, and push the opposite, which is really what the intention of this +// opcode is, it is extremely important that is not done because integers are +// interpreted differently than booleans and the consensus rules for this opcode +// dictate the item is interpreted as an integer. +// +// Stack transformation (x2==0): [... x1 0] -> [... x1 1] +// Stack transformation (x2!=0): [... x1 1] -> [... x1 0] +// Stack transformation (x2!=0): [... x1 17] -> [... x1 0] +func opcodeNot(op *opcode, data []byte, vm *Engine) error { + m, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if m == 0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcode0NotEqual treats the top item on the data stack as an integer and +// replaces it with either a 0 if it is zero, or a 1 if it is not zero. +// +// Stack transformation (x2==0): [... x1 0] -> [... x1 0] +// Stack transformation (x2!=0): [... x1 1] -> [... x1 1] +// Stack transformation (x2!=0): [... x1 17] -> [... x1 1] +func opcode0NotEqual(op *opcode, data []byte, vm *Engine) error { + m, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if m != 0 { + m = 1 + } + vm.dstack.PushInt(m) + return nil +} + +// opcodeAdd treats the top two items on the data stack as integers and replaces +// them with their sum. +// +// Stack transformation: [... x1 x2] -> [... x1+x2] +func opcodeAdd(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + vm.dstack.PushInt(v0 + v1) + return nil +} + +// opcodeSub treats the top two items on the data stack as integers and replaces +// them with the result of subtracting the top entry from the second-to-top +// entry. +// +// Stack transformation: [... x1 x2] -> [... x1-x2] +func opcodeSub(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + vm.dstack.PushInt(v1 - v0) + return nil +} + +// opcodeBoolAnd treats the top two items on the data stack as integers. When +// both of them are not zero, they are replaced with a 1, otherwise a 0. +// +// Stack transformation (x1==0, x2==0): [... 0 0] -> [... 0] +// Stack transformation (x1!=0, x2==0): [... 5 0] -> [... 0] +// Stack transformation (x1==0, x2!=0): [... 0 7] -> [... 0] +// Stack transformation (x1!=0, x2!=0): [... 4 8] -> [... 1] +func opcodeBoolAnd(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v0 != 0 && v1 != 0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + + return nil +} + +// opcodeBoolOr treats the top two items on the data stack as integers. When +// either of them are not zero, they are replaced with a 1, otherwise a 0. +// +// Stack transformation (x1==0, x2==0): [... 0 0] -> [... 0] +// Stack transformation (x1!=0, x2==0): [... 5 0] -> [... 1] +// Stack transformation (x1==0, x2!=0): [... 0 7] -> [... 1] +// Stack transformation (x1!=0, x2!=0): [... 4 8] -> [... 1] +func opcodeBoolOr(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v0 != 0 || v1 != 0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + + return nil +} + +// opcodeNumEqual treats the top two items on the data stack as integers. When +// they are equal, they are replaced with a 1, otherwise a 0. +// +// Stack transformation (x1==x2): [... 5 5] -> [... 1] +// Stack transformation (x1!=x2): [... 5 7] -> [... 0] +func opcodeNumEqual(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v0 == v1 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + + return nil +} + +// opcodeNumEqualVerify is a combination of opcodeNumEqual and opcodeVerify. +// +// Specifically, treats the top two items on the data stack as integers. When +// they are equal, they are replaced with a 1, otherwise a 0. Then, it examines +// the top item on the data stack as a boolean value and verifies it evaluates +// to true. An error is returned if it does not. +// +// Stack transformation: [... x1 x2] -> [... bool] -> [...] +func opcodeNumEqualVerify(op *opcode, data []byte, vm *Engine) error { + err := opcodeNumEqual(op, data, vm) + if err == nil { + err = abstractVerify(op, vm, txscript.ErrNumEqualVerify) + } + return err +} + +// opcodeNumNotEqual treats the top two items on the data stack as integers. +// When they are NOT equal, they are replaced with a 1, otherwise a 0. +// +// Stack transformation (x1==x2): [... 5 5] -> [... 0] +// Stack transformation (x1!=x2): [... 5 7] -> [... 1] +func opcodeNumNotEqual(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v0 != v1 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + + return nil +} + +// opcodeLessThan treats the top two items on the data stack as integers. When +// the second-to-top item is less than the top item, they are replaced with a 1, +// otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeLessThan(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v1 < v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + + return nil +} + +// opcodeGreaterThan treats the top two items on the data stack as integers. +// When the second-to-top item is greater than the top item, they are replaced +// with a 1, otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeGreaterThan(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v1 > v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeLessThanOrEqual treats the top two items on the data stack as integers. +// When the second-to-top item is less than or equal to the top item, they are +// replaced with a 1, otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeLessThanOrEqual(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v1 <= v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeGreaterThanOrEqual treats the top two items on the data stack as +// integers. When the second-to-top item is greater than or equal to the top +// item, they are replaced with a 1, otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeGreaterThanOrEqual(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v1 >= v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + + return nil +} + +// opcodeMin treats the top two items on the data stack as integers and replaces +// them with the minimum of the two. +// +// Stack transformation: [... x1 x2] -> [... min(x1, x2)] +func opcodeMin(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v1 < v0 { + vm.dstack.PushInt(v1) + } else { + vm.dstack.PushInt(v0) + } + + return nil +} + +// opcodeMax treats the top two items on the data stack as integers and replaces +// them with the maximum of the two. +// +// Stack transformation: [... x1 x2] -> [... max(x1, x2)] +func opcodeMax(op *opcode, data []byte, vm *Engine) error { + v0, err := vm.dstack.PopInt() + if err != nil { + return err + } + + v1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if v1 > v0 { + vm.dstack.PushInt(v1) + } else { + vm.dstack.PushInt(v0) + } + + return nil +} + +// opcodeWithin treats the top 3 items on the data stack as integers. When the +// value to test is within the specified range (left inclusive), they are +// replaced with a 1, otherwise a 0. +// +// The top item is the max value, the second-top-item is the minimum value, and +// the third-to-top item is the value to test. +// +// Stack transformation: [... x1 min max] -> [... bool] +func opcodeWithin(op *opcode, data []byte, vm *Engine) error { + maxVal, err := vm.dstack.PopInt() + if err != nil { + return err + } + + minVal, err := vm.dstack.PopInt() + if err != nil { + return err + } + + x, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if x >= minVal && x < maxVal { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// calcHash calculates the hash of hasher over buf. +func calcHash(buf []byte, hasher hash.Hash) []byte { + hasher.Write(buf) + return hasher.Sum(nil) +} + +// opcodeRipemd160 treats the top item of the data stack as raw bytes and +// replaces it with ripemd160(data). +// +// Stack transformation: [... x1] -> [... ripemd160(x1)] +func opcodeRipemd160(op *opcode, data []byte, vm *Engine) error { + buf, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + vm.dstack.PushByteArray(calcHash(buf, ripemd160.New())) + return nil +} + +// opcodeSha1 treats the top item of the data stack as raw bytes and replaces it +// with sha1(data). +// +// Stack transformation: [... x1] -> [... sha1(x1)] +func opcodeSha1(op *opcode, data []byte, vm *Engine) error { + buf, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + hash := sha1.Sum(buf) + vm.dstack.PushByteArray(hash[:]) + return nil +} + +// opcodeSha256 treats the top item of the data stack as raw bytes and replaces +// it with sha256(data). +// +// Stack transformation: [... x1] -> [... sha256(x1)] +func opcodeSha256(op *opcode, data []byte, vm *Engine) error { + buf, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + hash := sha256.Sum256(buf) + vm.dstack.PushByteArray(hash[:]) + return nil +} + +// opcodeHash160 treats the top item of the data stack as raw bytes and replaces +// it with ripemd160(sha256(data)). +// +// Stack transformation: [... x1] -> [... ripemd160(sha256(x1))] +func opcodeHash160(op *opcode, data []byte, vm *Engine) error { + buf, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + hash := sha256.Sum256(buf) + vm.dstack.PushByteArray(calcHash(hash[:], ripemd160.New())) + return nil +} + +// opcodeHash256 treats the top item of the data stack as raw bytes and replaces +// it with sha256(sha256(data)). +// +// Stack transformation: [... x1] -> [... sha256(sha256(x1))] +func opcodeHash256(op *opcode, data []byte, vm *Engine) error { + buf, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + vm.dstack.PushByteArray(chainhash.DoubleHashB(buf)) + return nil +} + +// opcodeCodeSeparator stores the current script offset as the most recently +// seen OP_CODESEPARATOR which is used during signature checking. +// +// This opcode does not change the contents of the data stack. +func opcodeCodeSeparator(op *opcode, data []byte, vm *Engine) error { + vm.lastCodeSep = int(vm.tokenizer.ByteIndex()) + + if vm.taprootCtx != nil { + vm.taprootCtx.codeSepPos = uint32(vm.tokenizer.OpcodePosition()) + } else if vm.witnessProgram == nil && + vm.hasFlag(txscript.ScriptVerifyConstScriptCode) { + + // Disable OP_CODESEPARATOR for non-segwit scripts. + str := "OP_CODESEPARATOR used in non-segwit script" + return scriptError(txscript.ErrCodeSeparator, str) + } + + return nil +} + +// opcodeCheckSig treats the top 2 items on the stack as a public key and a +// signature and replaces them with a bool which indicates if the signature was +// successfully verified. +// +// The process of verifying a signature requires calculating a signature hash in +// the same way the transaction signer did. It involves hashing portions of the +// transaction based on the hash type byte (which is the final byte of the +// signature) and the portion of the script starting from the most recent +// OP_CODESEPARATOR (or the beginning of the script if there are none) to the +// end of the script (with any other OP_CODESEPARATORs removed). Once this +// "script hash" is calculated, the signature is checked using standard +// cryptographic methods against the provided public key. +// +// Stack transformation: [... signature pubkey] -> [... bool] +func opcodeCheckSig(op *opcode, data []byte, vm *Engine) error { + pkBytes, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + fullSigBytes, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + // The signature actually needs to be longer than this, but at + // least 1 byte is needed for the hash type below. The full length is + // checked depending on the script flags and upon parsing the signature. + // + // This only applies if tapscript verification isn't active, as this + // check is done within the sighash itself. + if vm.taprootCtx == nil && len(fullSigBytes) < 1 { + vm.dstack.PushBool(false) + return nil + } + + var sigVerifier signatureVerifier + switch { + // If no witness program is active, then we're verifying under the + // base consensus rules. + case vm.witnessProgram == nil: + sigVerifier, err = newBaseSigVerifier( + pkBytes, fullSigBytes, vm, + ) + if err != nil { + var scriptErr txscript.Error + if errors.As(err, &scriptErr) { + return err + } + + vm.dstack.PushBool(false) + return nil + } + + // If the base segwit version is active, then we'll create the verifier + // that factors in those new consensus rules. + case vm.isWitnessVersionActive(txscript.BaseSegwitWitnessVersion): + sigVerifier, err = newBaseSegwitSigVerifier( + pkBytes, fullSigBytes, vm, + ) + if err != nil { + var scriptErr txscript.Error + if errors.As(err, &scriptErr) { + return err + } + + vm.dstack.PushBool(false) + return nil + } + + // Otherwise, this is routine tapscript execution. + case vm.taprootCtx != nil: + // Account for changes in the sig ops budget after this + // execution, but only for non-empty signatures. + if len(fullSigBytes) > 0 { + if err := vm.taprootCtx.tallysigOp(); err != nil { + return err + } + } + + // Empty public keys immediately cause execution to fail. + if len(pkBytes) == 0 { + return scriptError(txscript.ErrTaprootPubkeyIsEmpty, "") + } + + // If this is tapscript execution, and the signature was + // actually an empty vector, then we push on an empty vector + // and continue execution from there, but only if the pubkey + // isn't empty. + if len(fullSigBytes) == 0 { + vm.dstack.PushByteArray([]byte{}) + return nil + } + + // If the constructor fails immediately, then it's because + // the public key size is zero, so we'll fail all script + // execution. + sigVerifier, err = newBaseTapscriptSigVerifier( + pkBytes, fullSigBytes, vm, + ) + if err != nil { + return err + } + + default: + // We skip segwit v1 in isolation here, as the v1 rules aren't + // used in script execution (for sig verification) and are only + // part of the top-level key-spend verification which we + // already skipped. + // + // In other words, this path shouldn't ever be reached + // + // TODO(roasbeef): return an error? + } + + result := sigVerifier.Verify() + valid := result.sigValid + + if vm.hasFlag(txscript.ScriptVerifyConstScriptCode) && result.sigMatch { + str := "non-const script code" + return scriptError(txscript.ErrNonConstScriptCode, str) + } + + switch { + // For tapscript, and prior execution with null fail active, if the + // signature is invalid, then this MUST be an empty signature. + case !valid && vm.taprootCtx != nil && len(fullSigBytes) != 0: + fallthrough + case !valid && vm.hasFlag(txscript.ScriptVerifyNullFail) && len(fullSigBytes) > 0: + str := "signature not empty on failed checksig" + return scriptError(txscript.ErrNullFail, str) + } + + vm.dstack.PushBool(valid) + return nil +} + +// opcodeCheckSigVerify is a combination of opcodeCheckSig and opcodeVerify. +// The opcodeCheckSig function is invoked followed by opcodeVerify. See the +// documentation for each of those opcodes for more details. +// +// Stack transformation: [... signature pubkey] -> [... bool] -> [...] +func opcodeCheckSigVerify(op *opcode, data []byte, vm *Engine) error { + err := opcodeCheckSig(op, data, vm) + if err == nil { + err = abstractVerify(op, vm, txscript.ErrCheckSigVerify) + } + return err +} + +// opcodeCheckSigAdd implements the OP_CHECKSIGADD operation defined in BIP +// 342. This is a replacement for OP_CHECKMULTISIGVERIFY and OP_CHECKMULTISIG +// that lends better to batch sig validation, as well as a possible future of +// signature aggregation across inputs. +// +// The op code takes a public key, an integer (N) and a signature, and returns +// N if the signature was the empty vector, and n+1 otherwise. +// +// Stack transformation: [... pubkey n signature] -> [... n | n+1 ] -> [...] +func opcodeCheckSigAdd(op *opcode, data []byte, vm *Engine) error { + // This op code can only be used if tapsript execution is active. + // Before the soft fork, this opcode was marked as an invalid reserved + // op code. + if vm.taprootCtx == nil { + str := fmt.Sprintf("attempt to execute invalid opcode %s", op.name) + return scriptError(txscript.ErrReservedOpcode, str) + } + + // Pop the signature, integer n, and public key off the stack. + pubKeyBytes, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + accumulatorInt, err := vm.dstack.PopInt() + if err != nil { + return err + } + sigBytes, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + // Only non-empty signatures count towards the total tapscript sig op + // limit. + if len(sigBytes) != 0 { + // Account for changes in the sig ops budget after this execution. + if err := vm.taprootCtx.tallysigOp(); err != nil { + return err + } + } + + // Empty public keys immediately cause execution to fail. + if len(pubKeyBytes) == 0 { + return scriptError(txscript.ErrTaprootPubkeyIsEmpty, "") + } + + // If the signature is empty, then we'll just push the value N back + // onto the stack and continue from here. + if len(sigBytes) == 0 { + vm.dstack.PushInt(accumulatorInt) + return nil + } + + // Otherwise, we'll attempt to validate the signature as normal. + // + // If the constructor fails immediately, then it's because the public + // key size is zero, so we'll fail all script execution. + sigVerifier, err := newBaseTapscriptSigVerifier( + pubKeyBytes, sigBytes, vm, + ) + if err != nil { + return err + } + + result := sigVerifier.Verify() + + // If the signature is invalid, this we fail execution, as it should + // have been an empty signature. + if !result.sigValid { + str := "signature not empty on failed checksig" + return scriptError(txscript.ErrNullFail, str) + } + + // Otherwise, we increment the accumulatorInt by one, and push that + // back onto the stack. + vm.dstack.PushInt(accumulatorInt + 1) + + return nil +} + +// parsedSigInfo houses a raw signature along with its parsed form and a flag +// for whether or not it has already been parsed. It is used to prevent parsing +// the same signature multiple times when verifying a multisig. +type parsedSigInfo struct { + signature []byte + parsedSignature *ecdsa.Signature + parsed bool +} + +// opcodeCheckMultiSig treats the top item on the stack as an integer number of +// public keys, followed by that many entries as raw data representing the public +// keys, followed by the integer number of signatures, followed by that many +// entries as raw data representing the signatures. +// +// Due to a bug in the original Satoshi client implementation, an additional +// dummy argument is also required by the consensus rules, although it is not +// used. The dummy value SHOULD be an OP_0, although that is not required by +// the consensus rules. When the ScriptStrictMultiSig flag is set, it must be +// OP_0. +// +// All of the aforementioned stack items are replaced with a bool which +// indicates if the requisite number of signatures were successfully verified. +// +// See the opcodeCheckSigVerify documentation for more details about the process +// for verifying each signature. +// +// Stack transformation: +// [... dummy [sig ...] numsigs [pubkey ...] numpubkeys] -> [... bool] +func opcodeCheckMultiSig(op *opcode, data []byte, vm *Engine) error { + // If we're doing tapscript execution, then this op code is disabled. + if vm.taprootCtx != nil { + str := fmt.Sprintf("OP_CHECKMULTISIG and " + + "OP_CHECKMULTISIGVERIFY are disabled during " + + "tapscript execution") + return scriptError(txscript.ErrTapscriptCheckMultisig, str) + } + + numKeys, err := vm.dstack.PopInt() + if err != nil { + return err + } + + numPubKeys := int(numKeys.Int32()) + if numPubKeys < 0 { + str := fmt.Sprintf("number of pubkeys %d is negative", + numPubKeys) + return scriptError(txscript.ErrInvalidPubKeyCount, str) + } + if numPubKeys > txscript.MaxPubKeysPerMultiSig { + str := fmt.Sprintf("too many pubkeys: %d > %d", + numPubKeys, txscript.MaxPubKeysPerMultiSig) + return scriptError(txscript.ErrInvalidPubKeyCount, str) + } + vm.numOps += numPubKeys + if vm.numOps > txscript.MaxOpsPerScript { + str := fmt.Sprintf("exceeded max operation limit of %d", + txscript.MaxOpsPerScript) + return scriptError(txscript.ErrTooManyOperations, str) + } + + pubKeys := make([][]byte, 0, numPubKeys) + for i := 0; i < numPubKeys; i++ { + pubKey, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + pubKeys = append(pubKeys, pubKey) + } + + numSigs, err := vm.dstack.PopInt() + if err != nil { + return err + } + numSignatures := int(numSigs.Int32()) + if numSignatures < 0 { + str := fmt.Sprintf("number of signatures %d is negative", + numSignatures) + return scriptError(txscript.ErrInvalidSignatureCount, str) + + } + if numSignatures > numPubKeys { + str := fmt.Sprintf("more signatures than pubkeys: %d > %d", + numSignatures, numPubKeys) + return scriptError(txscript.ErrInvalidSignatureCount, str) + } + + signatures := make([]*parsedSigInfo, 0, numSignatures) + for i := 0; i < numSignatures; i++ { + signature, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + sigInfo := &parsedSigInfo{signature: signature} + signatures = append(signatures, sigInfo) + } + + // A bug in the original Satoshi client implementation means one more + // stack value than should be used must be popped. Unfortunately, this + // buggy behavior is now part of the consensus and a hard fork would be + // required to fix it. + dummy, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + // Since the dummy argument is otherwise not checked, it could be any + // value which unfortunately provides a source of malleability. Thus, + // there is a script flag to force an error when the value is NOT 0. + if vm.hasFlag(txscript.ScriptStrictMultiSig) && len(dummy) != 0 { + str := fmt.Sprintf("multisig dummy argument has length %d "+ + "instead of 0", len(dummy)) + return scriptError(txscript.ErrSigNullDummy, str) + } + + // Get script starting from the most recent OP_CODESEPARATOR. + script := vm.subScript() + + // Remove the signature in pre version 0 segwit scripts since there is + // no way for a signature to sign itself. + if !vm.isWitnessVersionActive(0) { + for _, sigInfo := range signatures { + var match bool + script, match = removeOpcodeByData(script, sigInfo.signature) + if vm.hasFlag(txscript.ScriptVerifyConstScriptCode) && match { + str := fmt.Sprintf("got match of %v in %v", sigInfo.signature, + script) + return scriptError(txscript.ErrNonConstScriptCode, str) + } + } + } + + success := true + numPubKeys++ + pubKeyIdx := -1 + signatureIdx := 0 + for numSignatures > 0 { + // When there are more signatures than public keys remaining, + // there is no way to succeed since too many signatures are + // invalid, so exit early. + pubKeyIdx++ + numPubKeys-- + if numSignatures > numPubKeys { + success = false + break + } + + sigInfo := signatures[signatureIdx] + pubKey := pubKeys[pubKeyIdx] + + // The order of the signature and public key evaluation is + // important here since it can be distinguished by an + // OP_CHECKMULTISIG NOT when the strict encoding flag is set. + + rawSig := sigInfo.signature + if len(rawSig) == 0 { + // Skip to the next pubkey if signature is empty. + continue + } + + // Split the signature into hash type and signature components. + hashType := txscript.SigHashType(rawSig[len(rawSig)-1]) + signature := rawSig[:len(rawSig)-1] + + // Only parse and check the signature encoding once. + var parsedSig *ecdsa.Signature + if !sigInfo.parsed { + if err := vm.checkHashTypeEncoding(hashType); err != nil { + return err + } + if err := vm.checkSignatureEncoding(signature); err != nil { + return err + } + + // Parse the signature. + var err error + if vm.hasFlag(txscript.ScriptVerifyStrictEncoding) || + vm.hasFlag(txscript.ScriptVerifyDERSignatures) { + + parsedSig, err = ecdsa.ParseDERSignature(signature) + } else { + parsedSig, err = ecdsa.ParseSignature(signature) + } + sigInfo.parsed = true + if err != nil { + continue + } + sigInfo.parsedSignature = parsedSig + } else { + // Skip to the next pubkey if the signature is invalid. + if sigInfo.parsedSignature == nil { + continue + } + + // Use the already parsed signature. + parsedSig = sigInfo.parsedSignature + } + + if err := vm.checkPubKeyEncoding(pubKey); err != nil { + return err + } + + // Parse the pubkey. + parsedPubKey, err := btcec.ParsePubKey(pubKey) + if err != nil { + continue + } + + // Generate the signature hash based on the signature hash type. + var hash []byte + if vm.isWitnessVersionActive(0) { + var sigHashes *txscript.TxSigHashes + if vm.hashCache != nil { + sigHashes = vm.hashCache + } else { + sigHashes = txscript.NewTxSigHashes( + &vm.tx, vm.prevOutFetcher, + ) + } + + hash, err = txscript.CalcWitnessSigHash( + script, sigHashes, hashType, + &vm.tx, vm.txIdx, vm.inputAmount, + ) + if err != nil { + return err + } + } else { + hash, err = txscript.CalcSignatureHash(script, hashType, &vm.tx, vm.txIdx) + if err != nil { + return err + } + } + + var valid bool + if vm.sigCache != nil { + var sigHash chainhash.Hash + copy(sigHash[:], hash) + + valid = vm.sigCache.Exists(sigHash, signature, pubKey) + if !valid && parsedSig.Verify(hash, parsedPubKey) { + vm.sigCache.Add(sigHash, signature, pubKey) + valid = true + } + } else { + valid = parsedSig.Verify(hash, parsedPubKey) + } + + if valid { + // PubKey verified, move on to the next signature. + signatureIdx++ + numSignatures-- + } + } + + if !success && vm.hasFlag(txscript.ScriptVerifyNullFail) { + for _, sig := range signatures { + if len(sig.signature) > 0 { + str := "not all signatures empty on failed checkmultisig" + return scriptError(txscript.ErrNullFail, str) + } + } + } + + vm.dstack.PushBool(success) + return nil +} + +// opcodeCheckMultiSigVerify is a combination of opcodeCheckMultiSig and +// opcodeVerify. The opcodeCheckMultiSig is invoked followed by opcodeVerify. +// See the documentation for each of those opcodes for more details. +// +// Stack transformation: +// [... dummy [sig ...] numsigs [pubkey ...] numpubkeys] -> [... bool] -> [...] +func opcodeCheckMultiSigVerify(op *opcode, data []byte, vm *Engine) error { + err := opcodeCheckMultiSig(op, data, vm) + if err == nil { + err = abstractVerify(op, vm, txscript.ErrCheckMultiSigVerify) + } + return err +} + +// OpcodeByName is a map that can be used to lookup an opcode by its +// human-readable name (OP_CHECKMULTISIG, OP_CHECKSIG, etc). +var OpcodeByName = make(map[string]byte) + +func init() { + // Initialize the opcode name to value map using the contents of the + // opcode array. Also add entries for "OP_FALSE", "OP_TRUE", and + // "OP_NOP2" since they are aliases for "OP_0", "OP_1", + // and "OP_CHECKLOCKTIMEVERIFY" respectively. + for _, op := range opcodeArray { + OpcodeByName[op.name] = op.value + } + OpcodeByName["OP_FALSE"] = OP_FALSE + OpcodeByName["OP_TRUE"] = OP_TRUE + OpcodeByName["OP_NOP2"] = OP_CHECKLOCKTIMEVERIFY + OpcodeByName["OP_NOP3"] = OP_CHECKSEQUENCEVERIFY +} + +// opcodeInspectInputOutpoint pops the input index from the stack and pushes the outpoint of the current input onto the stack. +// Stack transformation: [... index] -> [... txid index] +func opcodeInspectInputOutpoint(op *opcode, data []byte, vm *Engine) error { + index, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if int(index) >= len(vm.tx.TxIn) { + return scriptError(txscript.ErrInvalidIndex, "input index out of range") + } + + txIn := vm.tx.TxIn[index] + txid := make([]byte, 32) + copy(txid[:32], txIn.PreviousOutPoint.Hash[:]) + + vm.dstack.PushByteArray(txid) + vm.dstack.PushInt(scriptNum(txIn.PreviousOutPoint.Index)) + return nil +} + +// opcodeInspectInputValue pops the input index from the stack and pushes the value of the current input onto the stack. +// Stack transformation: [... index] -> [... value] +func opcodeInspectInputValue(op *opcode, data []byte, vm *Engine) error { + index, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if int(index) >= len(vm.tx.TxIn) { + return scriptError(txscript.ErrInvalidIndex, "input index out of range") + } + + if vm.prevOutFetcher == nil { + return scriptError(txscript.ErrInvalidIndex, "previous output fetcher not set") + } + + prevOut := vm.prevOutFetcher.FetchPrevOutput(vm.tx.TxIn[index].PreviousOutPoint) + + value := make([]byte, 8) + binary.LittleEndian.PutUint64(value, uint64(prevOut.Value)) + vm.dstack.PushByteArray(value) + return nil +} + +func pushScriptPubKey(scriptPubKey []byte, vm *Engine) error { + if txscript.IsWitnessProgram(scriptPubKey) { + version, program, err := txscript.ExtractWitnessProgramInfo(scriptPubKey) + if err != nil { + return err + } + + // push the witness program (2-40 bytes) + vm.dstack.PushByteArray(program) + + // push the segwit version as a scriptNum + vm.dstack.PushInt(scriptNum(version)) + return nil + } + + // non-native segwit, calculate sha256 hash + hash := sha256.Sum256(scriptPubKey) + vm.dstack.PushByteArray(hash[:]) + + // -1 indicate non-native segwit + vm.dstack.PushInt(-1) + return nil +} + +// opcodeInspectInputScriptPubkey pops the input index from the stack and pushes the scriptPubKey of the current input onto the stack. +// Stack transformation: [... index] -> [... scriptPubKey] +func opcodeInspectInputScriptPubkey(op *opcode, data []byte, vm *Engine) error { + index, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if int(index) >= len(vm.tx.TxIn) { + return scriptError(txscript.ErrInvalidIndex, "input index out of range") + } + + if vm.prevOutFetcher == nil { + return scriptError(txscript.ErrInvalidIndex, "previous output fetcher not set") + } + + prevOut := vm.prevOutFetcher.FetchPrevOutput(vm.tx.TxIn[index].PreviousOutPoint) + if prevOut == nil { + return scriptError(txscript.ErrInvalidIndex, "previous output not found") + } + + return pushScriptPubKey(prevOut.PkScript, vm) +} + +// opcodeInspectInputSequence pops the input index from the stack and pushes the sequence number of the current input onto the stack. +// Stack transformation: [...] -> [... sequence] +func opcodeInspectInputSequence(op *opcode, data []byte, vm *Engine) error { + index, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if int(index) >= len(vm.tx.TxIn) { + return scriptError(txscript.ErrInvalidIndex, "input index out of range") + } + + sequence := make([]byte, 4) + binary.LittleEndian.PutUint32(sequence, vm.tx.TxIn[index].Sequence) + vm.dstack.PushByteArray(sequence) + return nil +} + +// opcodePushCurrentInputIndex pushes the current input index onto the stack. +// Stack transformation: [...] -> [... index] +func opcodePushCurrentInputIndex(op *opcode, data []byte, vm *Engine) error { + vm.dstack.PushInt(scriptNum(vm.txIdx)) + return nil +} + +// opcodeInspectOutputValue pops the output index from the stack and pushes the value of the output onto the stack. +// Stack transformation: [... index] -> [... value] +func opcodeInspectOutputValue(op *opcode, data []byte, vm *Engine) error { + index, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if int(index) >= len(vm.tx.TxOut) { + return scriptError(txscript.ErrInvalidIndex, "output index out of range") + } + + value := make([]byte, 8) + binary.LittleEndian.PutUint64(value, uint64(vm.tx.TxOut[index].Value)) + vm.dstack.PushByteArray(value) + return nil +} + +// opcodeInspectOutputScriptPubkey pushes the scriptPubKey of the output at the given index onto the stack. +// Stack transformation: [... index] -> [... scriptPubKey] +func opcodeInspectOutputScriptPubkey(op *opcode, data []byte, vm *Engine) error { + index, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if int(index) >= len(vm.tx.TxOut) { + return scriptError(txscript.ErrInvalidIndex, "output index out of range") + } + + return pushScriptPubKey(vm.tx.TxOut[index].PkScript, vm) +} + +// opcodeInspectVersion pushes the transaction version onto the stack. +// Stack transformation: [...] -> [... version] +func opcodeInspectVersion(op *opcode, data []byte, vm *Engine) error { + version := make([]byte, 4) + binary.LittleEndian.PutUint32(version, uint32(vm.tx.Version)) + vm.dstack.PushByteArray(version) + return nil +} + +// opcodeInspectLocktime pushes the transaction locktime onto the stack. +// Stack transformation: [...] -> [... locktime] +func opcodeInspectLocktime(op *opcode, data []byte, vm *Engine) error { + locktime := make([]byte, 4) + binary.LittleEndian.PutUint32(locktime, vm.tx.LockTime) + vm.dstack.PushByteArray(locktime) + return nil +} + +// opcodeInspectNumInputs pushes the number of inputs in the transaction onto the stack. +// Stack transformation: [...] -> [... numInputs] +func opcodeInspectNumInputs(op *opcode, data []byte, vm *Engine) error { + vm.dstack.PushInt(scriptNum(len(vm.tx.TxIn))) + return nil +} + +// opcodeInspectNumOutputs pushes the number of outputs in the transaction onto the stack. +// Stack transformation: [...] -> [... numOutputs] +func opcodeInspectNumOutputs(op *opcode, data []byte, vm *Engine) error { + vm.dstack.PushInt(scriptNum(len(vm.tx.TxOut))) + return nil +} + +// opcodeTxWeight pushes the transaction weight onto the stack. +// Stack transformation: [...] -> [... weight] +func opcodeTxWeight(op *opcode, data []byte, vm *Engine) error { + weight := make([]byte, 4) + binary.LittleEndian.PutUint32(weight, uint32(vm.tx.SerializeSizeStripped()*4)) + vm.dstack.PushByteArray(weight) + return nil +} + +// opcodeCat concatenates two byte arrays. +// Stack transformation: [... x1 x2] -> [... x1|x2] +func opcodeCat(op *opcode, data []byte, vm *Engine) error { + x2, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + x1, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + vm.dstack.PushByteArray(append(x1, x2...)) + return nil +} + +// opcodeSubstr returns a portion of a byte array. +// Stack transformation: [... x n size] -> [... x[n:n+size]] +func opcodeSubstr(op *opcode, data []byte, vm *Engine) error { + size, err := vm.dstack.PopInt() + if err != nil { + return err + } + begin, err := vm.dstack.PopInt() + if err != nil { + return err + } + x, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + // Ensure bounds are valid + if begin < 0 || size < 0 || begin+size > scriptNum(len(x)) { + return scriptError(txscript.ErrInvalidIndex, "invalid substring parameters") + } + + vm.dstack.PushByteArray(x[begin : begin+size]) + return nil +} + +// opcodeLeft returns the first N bytes of a byte array. +// Stack transformation: [... x n] -> [... x[:n]] +func opcodeLeft(op *opcode, data []byte, vm *Engine) error { + n, err := vm.dstack.PopInt() + if err != nil { + return err + } + x, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + // Ensure bounds are valid + if n < 0 || n > scriptNum(len(x)) { + return scriptError(txscript.ErrInvalidIndex, "invalid left index") + } + + vm.dstack.PushByteArray(x[:n]) + return nil +} + +// opcodeRight returns the last N bytes of a byte array. +// Stack transformation: [... x n] -> [... x[len(x)-n:]] +func opcodeRight(op *opcode, data []byte, vm *Engine) error { + n, err := vm.dstack.PopInt() + if err != nil { + return err + } + x, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + // Ensure bounds are valid + if n < 0 || n > scriptNum(len(x)) { + return scriptError(txscript.ErrInvalidIndex, "invalid right index") + } + + vm.dstack.PushByteArray(x[len(x)-int(n):]) + return nil +} + +// opcodeInvert performs a bitwise NOT operation. +// Stack transformation: [... x] -> [... ~x] +func opcodeInvert(op *opcode, data []byte, vm *Engine) error { + x, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + result := make([]byte, len(x)) + for i := range x { + result[i] = ^x[i] + } + + vm.dstack.PushByteArray(result) + return nil +} + +// opcodeAnd performs a bitwise AND operation. +// Stack transformation: [... x1 x2] -> [... x1&x2] +func opcodeAnd(op *opcode, data []byte, vm *Engine) error { + x2, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + x1, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + if len(x1) != len(x2) { + return scriptError(txscript.ErrInvalidStackOperation, "mismatched operand sizes for AND") + } + + result := make([]byte, len(x1)) + for i := range x1 { + result[i] = x1[i] & x2[i] + } + + vm.dstack.PushByteArray(result) + return nil +} + +// opcodeOr performs a bitwise OR operation. +// Stack transformation: [... x1 x2] -> [... x1|x2] +func opcodeOr(op *opcode, data []byte, vm *Engine) error { + x2, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + x1, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + if len(x1) != len(x2) { + return scriptError(txscript.ErrInvalidStackOperation, "mismatched operand sizes for OR") + } + + result := make([]byte, len(x1)) + for i := range x1 { + result[i] = x1[i] | x2[i] + } + + vm.dstack.PushByteArray(result) + return nil +} + +// opcodeXor performs a bitwise XOR operation. +// Stack transformation: [... x1 x2] -> [... x1^x2] +func opcodeXor(op *opcode, data []byte, vm *Engine) error { + x2, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + x1, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + if len(x1) != len(x2) { + return scriptError(txscript.ErrInvalidStackOperation, "mismatched operand sizes for XOR") + } + + result := make([]byte, len(x1)) + for i := range x1 { + result[i] = x1[i] ^ x2[i] + } + + vm.dstack.PushByteArray(result) + return nil +} + +// opcode2Mul multiplies a number by 2. +// Stack transformation: [... x] -> [... x*2] +func opcode2Mul(op *opcode, data []byte, vm *Engine) error { + x, err := vm.dstack.PopInt() + if err != nil { + return err + } + + vm.dstack.PushInt(x * 2) + return nil +} + +// opcode2Div divides a number by 2. +// Stack transformation: [... x] -> [... x/2] +func opcode2Div(op *opcode, data []byte, vm *Engine) error { + x, err := vm.dstack.PopInt() + if err != nil { + return err + } + + vm.dstack.PushInt(x / 2) + return nil +} + +// opcodeMul multiplies two numbers. +// Stack transformation: [... x1 x2] -> [... x1*x2] +func opcodeMul(op *opcode, data []byte, vm *Engine) error { + x2, err := vm.dstack.PopInt() + if err != nil { + return err + } + x1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + vm.dstack.PushInt(x1 * x2) + return nil +} + +// opcodeDiv divides two numbers. +// Stack transformation: [... x1 x2] -> [... x1/x2] +func opcodeDiv(op *opcode, data []byte, vm *Engine) error { + x2, err := vm.dstack.PopInt() + if err != nil { + return err + } + x1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if x2 == 0 { + return scriptError(txscript.ErrInvalidStackOperation, "division by zero") + } + + vm.dstack.PushInt(x1 / x2) + return nil +} + +// opcodeMod returns the remainder after division. +// Stack transformation: [... x1 x2] -> [... x1%x2] +func opcodeMod(op *opcode, data []byte, vm *Engine) error { + x2, err := vm.dstack.PopInt() + if err != nil { + return err + } + x1, err := vm.dstack.PopInt() + if err != nil { + return err + } + + if x2 == 0 { + return scriptError(txscript.ErrInvalidStackOperation, "modulo by zero") + } + + vm.dstack.PushInt(x1 % x2) + return nil +} + +// opcodeLshift performs a left shift operation. +// Stack transformation: [... x n] -> [... x< [... x>>n] +func opcodeRshift(op *opcode, data []byte, vm *Engine) error { + n, err := vm.dstack.PopInt() + if err != nil { + return err + } + if n < 0 { + return scriptError(txscript.ErrInvalidIndex, "negative shift count") + } + + x, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + // Convert to big integer for arbitrary precision shift + value := new(big.Int).SetBytes(x) + result := value.Rsh(value, uint(n)) + + vm.dstack.PushByteArray(result.Bytes()) + return nil +} + +// opcodeChecksigFromStack verifies a signature against a public key and message from the stack. +// Stack transformation: [... sig msg pubkey] -> [... bool] +func opcodeChecksigFromStack(op *opcode, data []byte, vm *Engine) error { + pubKey, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + message, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + signature, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(signature) == 0 { + // push empty vector + vm.dstack.PushByteArray([]byte{}) + return nil + } + + if len(pubKey) == 0 { + return scriptError(txscript.ErrTaprootPubkeyIsEmpty, "public key is empty") + } + + if len(pubKey) != 32 { + return scriptError(txscript.ErrInvalidStackOperation, "invalid public key size, expected 32 bytes") + } + + pubKeyObj, err := schnorr.ParsePubKey(pubKey) + if err != nil { + return err + } + + signatureObj, err := schnorr.ParseSignature(signature) + if err != nil { + return err + } + + valid := signatureObj.Verify(message, pubKeyObj) + if !valid { + return scriptError(txscript.ErrNullFail, "schnorr signature verification failed") + } + + // success + vm.dstack.PushInt(1) + return nil +} + +// opcodeAdd64 performs 64-bit addition with overflow checking +// Stack transformation: [... a b] -> [... sum 1] (no overflow) or [... a b 0] (overflow) +func opcodeAdd64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_ADD64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_ADD64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + sum, overflow := mathutil.AddOverflowInt64(aVal, bVal) + if overflow { + // overflow : restore original operands and push 0 + vm.dstack.PushByteArray(a) + vm.dstack.PushByteArray(b) + vm.dstack.PushInt(0) + return nil + } + + // no overflow : push result and success scriptNum + result := make([]byte, 8) + binary.LittleEndian.PutUint64(result, uint64(sum)) + vm.dstack.PushByteArray(result) + vm.dstack.PushInt(1) + return nil +} + +// opcodeSub64 performs 64-bit subtraction with overflow checking +// Stack transformation: [... a b] -> [... diff 1] (no overflow) or [... a b 0] (overflow) +func opcodeSub64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_SUB64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_SUB64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + diff, overflow := mathutil.SubOverflowInt64(aVal, bVal) + if overflow { + // overflow : restore original operands and push 0 + vm.dstack.PushByteArray(a) + vm.dstack.PushByteArray(b) + vm.dstack.PushInt(0) + return nil + } + + // no overflow : push result and success scriptNum + result := make([]byte, 8) + binary.LittleEndian.PutUint64(result, uint64(diff)) + vm.dstack.PushByteArray(result) + vm.dstack.PushInt(1) + return nil +} + +// opcodeMul64 performs 64-bit multiplication with overflow checking +// Stack transformation: [... a b] -> [... product 1] (no overflow) or [... a b 0] (overflow) +func opcodeMul64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_MUL64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_MUL64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + product, overflow := mathutil.MulOverflowInt64(aVal, bVal) + if overflow { + // overflow : restore original operands and push 0 + vm.dstack.PushByteArray(a) + vm.dstack.PushByteArray(b) + vm.dstack.PushInt(0) + return nil + } + + // no overflow : push result and success scriptNum + result := make([]byte, 8) + binary.LittleEndian.PutUint64(result, uint64(product)) + vm.dstack.PushByteArray(result) + vm.dstack.PushInt(1) + return nil +} + +// opcodeDiv64 performs 64-bit division with overflow checking +// Stack transformation: [... a b] -> [... remainder quotient 1] (no overflow) or [... a b 0] (overflow) +func opcodeDiv64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_DIV64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_DIV64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + if bVal == 0 || (aVal == math.MinInt64 && bVal == -1) { + // division by zero or overflow, restore original operands and push 0 + vm.dstack.PushByteArray(a) + vm.dstack.PushByteArray(b) + vm.dstack.PushInt(0) + return nil + } + + quotient := aVal / bVal + remainder := aVal % bVal + + // ensure remainder is non-negative and less than |b| + if remainder < 0 { + remainder += int64(math.Abs(float64(bVal))) + quotient -= 1 + } + + remainderBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(remainderBytes, uint64(remainder)) + vm.dstack.PushByteArray(remainderBytes) + + quotientBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(quotientBytes, uint64(quotient)) + vm.dstack.PushByteArray(quotientBytes) + + vm.dstack.PushInt(1) + return nil +} + +// opcodeNeg64 performs 64-bit negation with overflow checking +// Stack transformation: [... a] -> [... -a 1] (no overflow) or [... a 0] (overflow) +func opcodeNeg64(op *opcode, data []byte, vm *Engine) error { + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_NEG64 requires 8-byte operand") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + + product, overflow := mathutil.MulOverflowInt64(aVal, -1) + if overflow { + // overflow : restore original operand and push 0 + vm.dstack.PushByteArray(a) + vm.dstack.PushInt(0) + return nil + } + + // no overflow : push result and success scriptNum + result := make([]byte, 8) + binary.LittleEndian.PutUint64(result, uint64(product)) + vm.dstack.PushByteArray(result) + vm.dstack.PushInt(1) + return nil +} + +// opcodeLessThan64 performs 64-bit less than comparison +// Stack transformation: [... a b] -> [... bool] +func opcodeLessThan64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_LESSTHAN64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_LESSTHAN64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + vm.dstack.PushBool(aVal < bVal) + return nil +} + +// opcodeLessThanOrEqual64 performs 64-bit less than or equal comparison +// Stack transformation: [... a b] -> [... bool] +func opcodeLessThanOrEqual64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_LESSTHANOREQUAL64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_LESSTHANOREQUAL64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + vm.dstack.PushBool(aVal <= bVal) + return nil +} + +// opcodeGreaterThan64 performs 64-bit greater than comparison +// Stack transformation: [... a b] -> [... bool] +func opcodeGreaterThan64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_GREATERTHAN64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_GREATERTHAN64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + vm.dstack.PushBool(aVal > bVal) + return nil +} + +// opcodeGreaterThanOrEqual64 performs 64-bit greater than or equal comparison +// Stack transformation: [... a b] -> [... bool] +func opcodeGreaterThanOrEqual64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_GREATERTHANOREQUAL64 requires 8-byte operands") + } + + a, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(a) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_GREATERTHANOREQUAL64 requires 8-byte operands") + } + + aVal := int64(binary.LittleEndian.Uint64(a)) + bVal := int64(binary.LittleEndian.Uint64(b)) + + vm.dstack.PushBool(aVal >= bVal) + return nil +} + +// opcodeScriptNumToLE64 converts a minimal CScriptNum to an 8-byte signed LE number +// Stack transformation: [... num] -> [... le64] +func opcodeScriptNumToLE64(op *opcode, data []byte, vm *Engine) error { + num, err := vm.dstack.PopInt() + if err != nil { + return err + } + + result := make([]byte, 8) + binary.LittleEndian.PutUint64(result, uint64(int64(num))) + vm.dstack.PushByteArray(result) + return nil +} + +// opcodeLE64ToScriptNum converts an 8-byte signed LE number to a minimal CScriptNum +// Stack transformation: [... le64] -> [... num] +func opcodeLE64ToScriptNum(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 8 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_LE64TOSCRIPTNUM requires 8-byte operand") + } + + val := int64(binary.LittleEndian.Uint64(b)) + vm.dstack.PushInt(scriptNum(val)) + return nil +} + +// opcodeLE32ToLE64 converts a 4-byte unsigned LE number to an 8-byte signed LE number +// Stack transformation: [... le32] -> [... le64] +func opcodeLE32ToLE64(op *opcode, data []byte, vm *Engine) error { + b, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + if len(b) != 4 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_LE32TOLE64 requires 4-byte operand") + } + + val := uint32(binary.LittleEndian.Uint32(b)) + result := make([]byte, 8) + binary.LittleEndian.PutUint64(result, uint64(val)) + vm.dstack.PushByteArray(result) + return nil +} + +// opcodeECMulScalarVerify verifies that Q = k*P where k is a 32-byte big endian scalar +// and P, Q are compressed EC points on the secp256k1 curve. +// Stack transformation: [... k P Q] -> [...] +func opcodeECMulScalarVerify(op *opcode, data []byte, vm *Engine) error { + Q, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + P, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + k, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + if len(k) != 32 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_ECMULSCALARVERIFY requires 32-byte scalar") + } + + pubKeyP, err := secp.ParsePubKey(P) + if err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "invalid point P") + } + + var scalar secp.ModNScalar + if overflow := scalar.SetByteSlice(k); overflow { + return scriptError(txscript.ErrInvalidStackOperation, "scalar k is outside of curve order") + } + + var point secp.JacobianPoint + point.X.SetByteSlice(pubKeyP.X().Bytes()) + point.Y.SetByteSlice(pubKeyP.Y().Bytes()) + point.Z.SetInt(1) + + // calculate k*P + var result secp.JacobianPoint + secp.ScalarMultNonConst(&scalar, &point, &result) + result.ToAffine() + + kP := secp.NewPublicKey(&result.X, &result.Y) + + // verify Q == k*P by comparing their serialized compressed forms + if !bytes.Equal(Q, kP.SerializeCompressed()) { + return scriptError(txscript.ErrInvalidStackOperation, "Q != k*P") + } + + return nil +} + +// opcodeTweakVerify verifies that Q = P + k*G where P is a 32-byte X-only internal key, +// k is a 32-byte big endian scalar, Q is a 33-byte compressed point, and G is the generator point. +// Stack transformation: [... P k Q] -> [...] +func opcodeTweakVerify(op *opcode, data []byte, vm *Engine) error { + Q, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + k, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + P, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + if len(P) != 32 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_TWEAKVERIFY requires 32-byte X-only key") + } + + if len(k) != 32 { + return scriptError(txscript.ErrInvalidStackOperation, "OP_TWEAKVERIFY requires 32-byte scalar") + } + + var scalar secp.ModNScalar + if overflow := scalar.SetByteSlice(k); overflow { + return scriptError(txscript.ErrInvalidStackOperation, "scalar k is outside of curve order") + } + + var kG secp.JacobianPoint + secp.ScalarBaseMultNonConst(&scalar, &kG) + kG.ToAffine() + + var px secp.FieldVal + if overflow := px.SetByteSlice(P); overflow { + return scriptError(txscript.ErrInvalidStackOperation, "invalid X-only key P") + } + + var py secp.FieldVal + if !secp.DecompressY(&px, false, &py) { + return scriptError(txscript.ErrInvalidStackOperation, "invalid X-only key P") + } + + var pointP secp.JacobianPoint + pointP.X = px + pointP.Y = py + pointP.Z.SetInt(1) + + var result secp.JacobianPoint + secp.AddNonConst(&pointP, &kG, &result) + result.ToAffine() + + tweakedKey := secp.NewPublicKey(&result.X, &result.Y) + + // verify Q == P + k*G by comparing their serialized compressed forms + if !bytes.Equal(Q, tweakedKey.SerializeCompressed()) { + return scriptError(txscript.ErrInvalidStackOperation, "Q != P + k*G") + } + + return nil +} + +// opcodeSha256Initialize pops a bytestring and pushes a SHA256 context created by adding +// the bytestring to the initial SHA256 context. +func opcodeSha256Initialize(op *opcode, _ []byte, vm *Engine) error { + data, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + h := sha256.New() + if _, err := h.Write(data); err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "failed to write to SHA256 context") + } + + var state bytes.Buffer + if err := gob.NewEncoder(&state).Encode(h); err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "failed to save hash state") + } + + vm.dstack.PushByteArray(state.Bytes()) + return nil +} + +// opcodeSha256Update pops a bytestring and SHA256 context, then pushes an updated +// context by adding the bytestring to the data stream being hashed. +func opcodeSha256Update(op *opcode, _ []byte, vm *Engine) error { + data, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + state, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + h := sha256.New() + if err := gob.NewDecoder(bytes.NewReader(state)).Decode(h); err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "failed to load hash state") + } + + if _, err := h.Write(data); err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "failed to write data to SHA256") + } + + var newState bytes.Buffer + if err := gob.NewEncoder(&newState).Encode(h); err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "failed to save hash state") + } + + vm.dstack.PushByteArray(newState.Bytes()) + return nil +} + +// opcodeSha256Finalize pops a bytestring and SHA256 context, then pushes the final +// SHA256 hash value after adding the bytestring and completing the padding. +func opcodeSha256Finalize(op *opcode, _ []byte, vm *Engine) error { + data, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + state, err := vm.dstack.PopByteArray() + if err != nil { + return err + } + + h := sha256.New() + if err := gob.NewDecoder(bytes.NewReader(state)).Decode(h); err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "failed to load hash state") + } + if _, err := h.Write(data); err != nil { + return scriptError(txscript.ErrInvalidStackOperation, "failed to write data to SHA256") + } + + vm.dstack.PushByteArray(h.Sum(nil)) + return nil +} diff --git a/pkg/ark-lib/arkade/opcode_test.go b/pkg/ark-lib/arkade/opcode_test.go new file mode 100644 index 000000000..5063b07c3 --- /dev/null +++ b/pkg/ark-lib/arkade/opcode_test.go @@ -0,0 +1,211 @@ +// Copyright (c) 2013-2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package arkade + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" +) + +// TestOpcodeDisasm tests the print function for all opcodes in both the oneline +// and full modes to ensure it provides the expected disassembly. +func TestOpcodeDisasm(t *testing.T) { + t.Parallel() + + // First, test the oneline disassembly. + + // The expected strings for the data push opcodes are replaced in the + // test loops below since they involve repeating bytes. Also, the + // OP_NOP# and OP_UNKNOWN# are replaced below too, since it's easier + // than manually listing them here. + oneBytes := []byte{0x01} + oneStr := "01" + expectedStrings := [256]string{0x00: "0", 0x4f: "-1", + 0x50: "OP_RESERVED", 0x61: "OP_NOP", 0x62: "OP_VER", + 0x63: "OP_IF", 0x64: "OP_NOTIF", 0x65: "OP_VERIF", + 0x66: "OP_VERNOTIF", 0x67: "OP_ELSE", 0x68: "OP_ENDIF", + 0x69: "OP_VERIFY", 0x6a: "OP_RETURN", 0x6b: "OP_TOALTSTACK", + 0x6c: "OP_FROMALTSTACK", 0x6d: "OP_2DROP", 0x6e: "OP_2DUP", + 0x6f: "OP_3DUP", 0x70: "OP_2OVER", 0x71: "OP_2ROT", + 0x72: "OP_2SWAP", 0x73: "OP_IFDUP", 0x74: "OP_DEPTH", + 0x75: "OP_DROP", 0x76: "OP_DUP", 0x77: "OP_NIP", + 0x78: "OP_OVER", 0x79: "OP_PICK", 0x7a: "OP_ROLL", + 0x7b: "OP_ROT", 0x7c: "OP_SWAP", 0x7d: "OP_TUCK", + 0x7e: "OP_CAT", 0x7f: "OP_SUBSTR", 0x80: "OP_LEFT", + 0x81: "OP_RIGHT", 0x82: "OP_SIZE", 0x83: "OP_INVERT", + 0x84: "OP_AND", 0x85: "OP_OR", 0x86: "OP_XOR", + 0x87: "OP_EQUAL", 0x88: "OP_EQUALVERIFY", 0x89: "OP_RESERVED1", + 0x8a: "OP_RESERVED2", 0x8b: "OP_1ADD", 0x8c: "OP_1SUB", + 0x8d: "OP_2MUL", 0x8e: "OP_2DIV", 0x8f: "OP_NEGATE", + 0x90: "OP_ABS", 0x91: "OP_NOT", 0x92: "OP_0NOTEQUAL", + 0x93: "OP_ADD", 0x94: "OP_SUB", 0x95: "OP_MUL", 0x96: "OP_DIV", + 0x97: "OP_MOD", 0x98: "OP_LSHIFT", 0x99: "OP_RSHIFT", + 0x9a: "OP_BOOLAND", 0x9b: "OP_BOOLOR", 0x9c: "OP_NUMEQUAL", + 0x9d: "OP_NUMEQUALVERIFY", 0x9e: "OP_NUMNOTEQUAL", + 0x9f: "OP_LESSTHAN", 0xa0: "OP_GREATERTHAN", + 0xa1: "OP_LESSTHANOREQUAL", 0xa2: "OP_GREATERTHANOREQUAL", + 0xa3: "OP_MIN", 0xa4: "OP_MAX", 0xa5: "OP_WITHIN", + 0xa6: "OP_RIPEMD160", 0xa7: "OP_SHA1", 0xa8: "OP_SHA256", + 0xa9: "OP_HASH160", 0xaa: "OP_HASH256", 0xab: "OP_CODESEPARATOR", + 0xac: "OP_CHECKSIG", 0xad: "OP_CHECKSIGVERIFY", + 0xae: "OP_CHECKMULTISIG", 0xaf: "OP_CHECKMULTISIGVERIFY", + 0xfa: "OP_SMALLINTEGER", 0xfb: "OP_PUBKEYS", + 0xfd: "OP_PUBKEYHASH", 0xfe: "OP_PUBKEY", + 0xff: "OP_INVALIDOPCODE", 0xba: "OP_CHECKSIGADD", + // Add new defined opcodes + 0xc4: "OP_SHA256INITIALIZE", 0xc5: "OP_SHA256UPDATE", + 0xc6: "OP_SHA256FINALIZE", 0xc7: "OP_INSPECTINPUTOUTPOINT", + 0xc9: "OP_INSPECTINPUTVALUE", 0xca: "OP_INSPECTINPUTSCRIPTPUBKEY", + 0xcb: "OP_INSPECTINPUTSEQUENCE", 0xcc: "OP_CHECKSIGFROMSTACK", + 0xcd: "OP_PUSHCURRENTINPUTINDEX", 0xcf: "OP_INSPECTOUTPUTVALUE", + 0xd1: "OP_INSPECTOUTPUTSCRIPTPUBKEY", 0xd2: "OP_INSPECTVERSION", + 0xd3: "OP_INSPECTLOCKTIME", 0xd4: "OP_INSPECTNUMINPUTS", + 0xd5: "OP_INSPECTNUMOUTPUTS", 0xd6: "OP_TXWEIGHT", + 0xd7: "OP_ADD64", 0xd8: "OP_SUB64", 0xd9: "OP_MUL64", + 0xda: "OP_DIV64", 0xdb: "OP_NEG64", 0xdc: "OP_LESSTHAN64", + 0xdd: "OP_LESSTHANOREQUAL64", 0xde: "OP_GREATERTHAN64", + 0xdf: "OP_GREATERTHANOREQUAL64", 0xe0: "OP_SCRIPTNUMTOLE64", + 0xe1: "OP_LE64TOSCRIPTNUM", 0xe2: "OP_LE32TOLE64", + 0xe3: "OP_ECMULSCALARVERIFY", 0xe4: "OP_TWEAKVERIFY", + } + for opcodeVal, expectedStr := range expectedStrings { + var data []byte + switch { + // OP_DATA_1 through OP_DATA_65 display the pushed data. + case opcodeVal >= 0x01 && opcodeVal < 0x4c: + data = bytes.Repeat(oneBytes, opcodeVal) + expectedStr = strings.Repeat(oneStr, opcodeVal) + + // OP_PUSHDATA1. + case opcodeVal == 0x4c: + data = bytes.Repeat(oneBytes, 1) + expectedStr = strings.Repeat(oneStr, 1) + + // OP_PUSHDATA2. + case opcodeVal == 0x4d: + data = bytes.Repeat(oneBytes, 2) + expectedStr = strings.Repeat(oneStr, 2) + + // OP_PUSHDATA4. + case opcodeVal == 0x4e: + data = bytes.Repeat(oneBytes, 3) + expectedStr = strings.Repeat(oneStr, 3) + + // OP_1 through OP_16 display the numbers themselves. + case opcodeVal >= 0x51 && opcodeVal <= 0x60: + val := byte(opcodeVal - (0x51 - 1)) + data = []byte{val} + expectedStr = strconv.Itoa(int(val)) + + // OP_NOP1 through OP_NOP10. + case opcodeVal >= 0xb0 && opcodeVal <= 0xb9: + switch opcodeVal { + case 0xb1: + // OP_NOP2 is an alias of OP_CHECKLOCKTIMEVERIFY + expectedStr = "OP_CHECKLOCKTIMEVERIFY" + case 0xb2: + // OP_NOP3 is an alias of OP_CHECKSEQUENCEVERIFY + expectedStr = "OP_CHECKSEQUENCEVERIFY" + default: + val := byte(opcodeVal - (0xb0 - 1)) + expectedStr = "OP_NOP" + strconv.Itoa(int(val)) + } + + // OP_UNKNOWN#. + case (opcodeVal >= 0xbb && opcodeVal <= 0xc3) || // Unknown range before SHA256 ops + (opcodeVal == 0xc8) || // Unknown between input inspection ops + (opcodeVal == 0xce) || // Unknown between input and output ops + (opcodeVal == 0xd0) || // Unknown between output ops + (opcodeVal >= 0xe5 && opcodeVal <= 0xf9) || // Unknown range after new ops + opcodeVal == 0xfc: + expectedStr = "OP_UNKNOWN" + strconv.Itoa(opcodeVal) + } + + var buf strings.Builder + disasmOpcode(&buf, &opcodeArray[opcodeVal], data, true) + gotStr := buf.String() + if gotStr != expectedStr { + t.Errorf("pop.print (opcode %x): Unexpected disasm "+ + "string - got %v, want %v", opcodeVal, gotStr, + expectedStr) + continue + } + } + + // Now, replace the relevant fields and test the full disassembly. + expectedStrings[0x00] = "OP_0" + expectedStrings[0x4f] = "OP_1NEGATE" + for opcodeVal, expectedStr := range expectedStrings { + var data []byte + switch { + // OP_DATA_1 through OP_DATA_65 display the opcode followed by + // the pushed data. + case opcodeVal >= 0x01 && opcodeVal < 0x4c: + data = bytes.Repeat(oneBytes, opcodeVal) + expectedStr = fmt.Sprintf("OP_DATA_%d 0x%s", opcodeVal, + strings.Repeat(oneStr, opcodeVal)) + + // OP_PUSHDATA1. + case opcodeVal == 0x4c: + data = bytes.Repeat(oneBytes, 1) + expectedStr = fmt.Sprintf("OP_PUSHDATA1 0x%02x 0x%s", + len(data), strings.Repeat(oneStr, 1)) + + // OP_PUSHDATA2. + case opcodeVal == 0x4d: + data = bytes.Repeat(oneBytes, 2) + expectedStr = fmt.Sprintf("OP_PUSHDATA2 0x%04x 0x%s", + len(data), strings.Repeat(oneStr, 2)) + + // OP_PUSHDATA4. + case opcodeVal == 0x4e: + data = bytes.Repeat(oneBytes, 3) + expectedStr = fmt.Sprintf("OP_PUSHDATA4 0x%08x 0x%s", + len(data), strings.Repeat(oneStr, 3)) + + // OP_1 through OP_16. + case opcodeVal >= 0x51 && opcodeVal <= 0x60: + val := byte(opcodeVal - (0x51 - 1)) + data = []byte{val} + expectedStr = "OP_" + strconv.Itoa(int(val)) + + // OP_NOP1 through OP_NOP10. + case opcodeVal >= 0xb0 && opcodeVal <= 0xb9: + switch opcodeVal { + case 0xb1: + // OP_NOP2 is an alias of OP_CHECKLOCKTIMEVERIFY + expectedStr = "OP_CHECKLOCKTIMEVERIFY" + case 0xb2: + // OP_NOP3 is an alias of OP_CHECKSEQUENCEVERIFY + expectedStr = "OP_CHECKSEQUENCEVERIFY" + default: + val := byte(opcodeVal - (0xb0 - 1)) + expectedStr = "OP_NOP" + strconv.Itoa(int(val)) + } + + // OP_UNKNOWN#. + case (opcodeVal >= 0xbb && opcodeVal <= 0xc3) || // Unknown range before SHA256 ops + (opcodeVal == 0xc8) || // Unknown between input inspection ops + (opcodeVal == 0xce) || // Unknown between input and output ops + (opcodeVal == 0xd0) || // Unknown between output ops + (opcodeVal >= 0xe5 && opcodeVal <= 0xf9) || // Unknown range after new ops + opcodeVal == 0xfc: + expectedStr = "OP_UNKNOWN" + strconv.Itoa(opcodeVal) + } + + var buf strings.Builder + disasmOpcode(&buf, &opcodeArray[opcodeVal], data, false) + gotStr := buf.String() + if gotStr != expectedStr { + t.Errorf("pop.print (opcode %x): Unexpected disasm "+ + "string - got %v, want %v", opcodeVal, gotStr, + expectedStr) + continue + } + } +} diff --git a/pkg/ark-lib/arkade/scriptnum.go b/pkg/ark-lib/arkade/scriptnum.go new file mode 100644 index 000000000..1ccd67616 --- /dev/null +++ b/pkg/ark-lib/arkade/scriptnum.go @@ -0,0 +1,224 @@ +package arkade + +import ( + "fmt" + + "github.com/btcsuite/btcd/txscript" +) + +const ( + maxInt32 = 1<<31 - 1 + minInt32 = -1 << 31 + + // maxScriptNumLen is the maximum number of bytes data being interpreted + // as an integer may be for the majority of op codes. + maxScriptNumLen = 4 +) + +// scriptNum represents a numeric value used in the scripting engine with +// special handling to deal with the subtle semantics required by consensus. +// +// All numbers are stored on the data and alternate stacks encoded as little +// endian with a sign bit. All numeric opcodes such as OP_ADD, OP_SUB, +// and OP_MUL, are only allowed to operate on 4-byte integers in the range +// [-2^31 + 1, 2^31 - 1], however the results of numeric operations may overflow +// and remain valid so long as they are not used as inputs to other numeric +// operations or otherwise interpreted as an integer. +// +// For example, it is possible for OP_ADD to have 2^31 - 1 for its two operands +// resulting 2^32 - 2, which overflows, but is still pushed to the stack as the +// result of the addition. That value can then be used as input to OP_VERIFY +// which will succeed because the data is being interpreted as a boolean. +// However, if that same value were to be used as input to another numeric +// opcode, such as OP_SUB, it must fail. +// +// This type handles the aforementioned requirements by storing all numeric +// operation results as an int64 to handle overflow and provides the Bytes +// method to get the serialized representation (including values that overflow). +// +// Then, whenever data is interpreted as an integer, it is converted to this +// type by using the MakeScriptNum function which will return an error if the +// number is out of range or not minimally encoded depending on parameters. +// Since all numeric opcodes involve pulling data from the stack and +// interpreting it as an integer, it provides the required behavior. +type scriptNum int64 + +// checkMinimalDataEncoding returns whether or not the passed byte array adheres +// to the minimal encoding requirements. +func checkMinimalDataEncoding(v []byte) error { + if len(v) == 0 { + return nil + } + + // Check that the number is encoded with the minimum possible + // number of bytes. + // + // If the most-significant-byte - excluding the sign bit - is zero + // then we're not minimal. Note how this test also rejects the + // negative-zero encoding, [0x80]. + if v[len(v)-1]&0x7f == 0 { + // One exception: if there's more than one byte and the most + // significant bit of the second-most-significant-byte is set + // it would conflict with the sign bit. An example of this case + // is +-255, which encode to 0xff00 and 0xff80 respectively. + // (big-endian). + if len(v) == 1 || v[len(v)-2]&0x80 == 0 { + str := fmt.Sprintf("numeric value encoded as %x is "+ + "not minimally encoded", v) + return scriptError(txscript.ErrMinimalData, str) + } + } + + return nil +} + +// Bytes returns the number serialized as a little endian with a sign bit. +// +// Example encodings: +// +// 127 -> [0x7f] +// -127 -> [0xff] +// 128 -> [0x80 0x00] +// -128 -> [0x80 0x80] +// 129 -> [0x81 0x00] +// -129 -> [0x81 0x80] +// 256 -> [0x00 0x01] +// -256 -> [0x00 0x81] +// 32767 -> [0xff 0x7f] +// -32767 -> [0xff 0xff] +// 32768 -> [0x00 0x80 0x00] +// -32768 -> [0x00 0x80 0x80] +func (n scriptNum) Bytes() []byte { + // Zero encodes as an empty byte slice. + if n == 0 { + return nil + } + + // Take the absolute value and keep track of whether it was originally + // negative. + isNegative := n < 0 + if isNegative { + n = -n + } + + // Encode to little endian. The maximum number of encoded bytes is 9 + // (8 bytes for max int64 plus a potential byte for sign extension). + result := make([]byte, 0, 9) + for n > 0 { + result = append(result, byte(n&0xff)) + n >>= 8 + } + + // When the most significant byte already has the high bit set, an + // additional high byte is required to indicate whether the number is + // negative or positive. The additional byte is removed when converting + // back to an integral and its high bit is used to denote the sign. + // + // Otherwise, when the most significant byte does not already have the + // high bit set, use it to indicate the value is negative, if needed. + if result[len(result)-1]&0x80 != 0 { + extraByte := byte(0x00) + if isNegative { + extraByte = 0x80 + } + result = append(result, extraByte) + + } else if isNegative { + result[len(result)-1] |= 0x80 + } + + return result +} + +// Int32 returns the script number clamped to a valid int32. That is to say +// when the script number is higher than the max allowed int32, the max int32 +// value is returned and vice versa for the minimum value. Note that this +// behavior is different from a simple int32 cast because that truncates +// and the consensus rules dictate numbers which are directly cast to ints +// provide this behavior. +// +// In practice, for most opcodes, the number should never be out of range since +// it will have been created with MakeScriptNum using the defaultScriptLen +// value, which rejects them. In case something in the future ends up calling +// this function against the result of some arithmetic, which IS allowed to be +// out of range before being reinterpreted as an integer, this will provide the +// correct behavior. +func (n scriptNum) Int32() int32 { + if n > maxInt32 { + return maxInt32 + } + + if n < minInt32 { + return minInt32 + } + + return int32(n) +} + +// MakeScriptNum interprets the passed serialized bytes as an encoded integer +// and returns the result as a script number. +// +// Since the consensus rules dictate that serialized bytes interpreted as ints +// are only allowed to be in the range determined by a maximum number of bytes, +// on a per opcode basis, an error will be returned when the provided bytes +// would result in a number outside of that range. In particular, the range for +// the vast majority of opcodes dealing with numeric values are limited to 4 +// bytes and therefore will pass that value to this function resulting in an +// allowed range of [-2^31 + 1, 2^31 - 1]. +// +// The requireMinimal flag causes an error to be returned if additional checks +// on the encoding determine it is not represented with the smallest possible +// number of bytes or is the negative 0 encoding, [0x80]. For example, consider +// the number 127. It could be encoded as [0x7f], [0x7f 0x00], +// [0x7f 0x00 0x00 ...], etc. All forms except [0x7f] will return an error with +// requireMinimal enabled. +// +// The scriptNumLen is the maximum number of bytes the encoded value can be +// before an ErrStackNumberTooBig is returned. This effectively limits the +// range of allowed values. +// WARNING: Great care should be taken if passing a value larger than +// maxScriptNumLen, which could lead to addition and multiplication +// overflows. +// +// See the Bytes function documentation for example encodings. +func MakeScriptNum(v []byte, requireMinimal bool, scriptNumLen int) (scriptNum, error) { + // Interpreting data requires that it is not larger than + // the passed scriptNumLen value. + if len(v) > scriptNumLen { + str := fmt.Sprintf("numeric value encoded as %x is %d bytes "+ + "which exceeds the max allowed of %d", v, len(v), + scriptNumLen) + return 0, scriptError(txscript.ErrNumberTooBig, str) + } + + // Enforce minimal encoded if requested. + if requireMinimal { + if err := checkMinimalDataEncoding(v); err != nil { + return 0, err + } + } + + // Zero is encoded as an empty byte slice. + if len(v) == 0 { + return 0, nil + } + + // Decode from little endian. + var result int64 + for i, val := range v { + result |= int64(val) << uint8(8*i) + } + + // When the most significant byte of the input bytes has the sign bit + // set, the result is negative. So, remove the sign bit from the result + // and make it negative. + if v[len(v)-1]&0x80 != 0 { + // The maximum length of v has already been determined to be 4 + // above, so uint8 is enough to cover the max possible shift + // value of 24. + result &= ^(int64(0x80) << uint8(8*(len(v)-1))) + return scriptNum(-result), nil + } + + return scriptNum(result), nil +} diff --git a/pkg/ark-lib/arkade/scriptnum_test.go b/pkg/ark-lib/arkade/scriptnum_test.go new file mode 100644 index 000000000..f43b58806 --- /dev/null +++ b/pkg/ark-lib/arkade/scriptnum_test.go @@ -0,0 +1,275 @@ +// Copyright (c) 2015-2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package arkade + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/txscript" +) + +// hexToBytes converts the passed hex string into bytes and will panic if there +// is an error. This is only provided for the hard-coded constants so errors in +// the source code can be detected. It will only (and must only) be called with +// hard-coded values. +func hexToBytes(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex in source file: " + s) + } + return b +} + +// TestScriptNumBytes ensures that converting from integral script numbers to +// byte representations works as expected. +func TestScriptNumBytes(t *testing.T) { + t.Parallel() + + tests := []struct { + num scriptNum + serialized []byte + }{ + {0, nil}, + {1, hexToBytes("01")}, + {-1, hexToBytes("81")}, + {127, hexToBytes("7f")}, + {-127, hexToBytes("ff")}, + {128, hexToBytes("8000")}, + {-128, hexToBytes("8080")}, + {129, hexToBytes("8100")}, + {-129, hexToBytes("8180")}, + {256, hexToBytes("0001")}, + {-256, hexToBytes("0081")}, + {32767, hexToBytes("ff7f")}, + {-32767, hexToBytes("ffff")}, + {32768, hexToBytes("008000")}, + {-32768, hexToBytes("008080")}, + {65535, hexToBytes("ffff00")}, + {-65535, hexToBytes("ffff80")}, + {524288, hexToBytes("000008")}, + {-524288, hexToBytes("000088")}, + {7340032, hexToBytes("000070")}, + {-7340032, hexToBytes("0000f0")}, + {8388608, hexToBytes("00008000")}, + {-8388608, hexToBytes("00008080")}, + {2147483647, hexToBytes("ffffff7f")}, + {-2147483647, hexToBytes("ffffffff")}, + + // Values that are out of range for data that is interpreted as + // numbers, but are allowed as the result of numeric operations. + {2147483648, hexToBytes("0000008000")}, + {-2147483648, hexToBytes("0000008080")}, + {2415919104, hexToBytes("0000009000")}, + {-2415919104, hexToBytes("0000009080")}, + {4294967295, hexToBytes("ffffffff00")}, + {-4294967295, hexToBytes("ffffffff80")}, + {4294967296, hexToBytes("0000000001")}, + {-4294967296, hexToBytes("0000000081")}, + {281474976710655, hexToBytes("ffffffffffff00")}, + {-281474976710655, hexToBytes("ffffffffffff80")}, + {72057594037927935, hexToBytes("ffffffffffffff00")}, + {-72057594037927935, hexToBytes("ffffffffffffff80")}, + {9223372036854775807, hexToBytes("ffffffffffffff7f")}, + {-9223372036854775807, hexToBytes("ffffffffffffffff")}, + } + + for _, test := range tests { + gotBytes := test.num.Bytes() + if !bytes.Equal(gotBytes, test.serialized) { + t.Errorf("Bytes: did not get expected bytes for %d - "+ + "got %x, want %x", test.num, gotBytes, + test.serialized) + continue + } + } +} + +// TestMakeScriptNum ensures that converting from byte representations to +// integral script numbers works as expected. +func TestMakeScriptNum(t *testing.T) { + t.Parallel() + + // Errors used in the tests below defined here for convenience and to + // keep the horizontal test size shorter. + errNumTooBig := scriptError(txscript.ErrNumberTooBig, "") + errMinimalData := scriptError(txscript.ErrMinimalData, "") + + tests := []struct { + serialized []byte + num scriptNum + numLen int + minimalEncoding bool + err error + }{ + // Minimal encoding must reject negative 0. + {hexToBytes("80"), 0, maxScriptNumLen, true, errMinimalData}, + + // Minimally encoded valid values with minimal encoding flag. + // Should not error and return expected integral number. + {nil, 0, maxScriptNumLen, true, nil}, + {hexToBytes("01"), 1, maxScriptNumLen, true, nil}, + {hexToBytes("81"), -1, maxScriptNumLen, true, nil}, + {hexToBytes("7f"), 127, maxScriptNumLen, true, nil}, + {hexToBytes("ff"), -127, maxScriptNumLen, true, nil}, + {hexToBytes("8000"), 128, maxScriptNumLen, true, nil}, + {hexToBytes("8080"), -128, maxScriptNumLen, true, nil}, + {hexToBytes("8100"), 129, maxScriptNumLen, true, nil}, + {hexToBytes("8180"), -129, maxScriptNumLen, true, nil}, + {hexToBytes("0001"), 256, maxScriptNumLen, true, nil}, + {hexToBytes("0081"), -256, maxScriptNumLen, true, nil}, + {hexToBytes("ff7f"), 32767, maxScriptNumLen, true, nil}, + {hexToBytes("ffff"), -32767, maxScriptNumLen, true, nil}, + {hexToBytes("008000"), 32768, maxScriptNumLen, true, nil}, + {hexToBytes("008080"), -32768, maxScriptNumLen, true, nil}, + {hexToBytes("ffff00"), 65535, maxScriptNumLen, true, nil}, + {hexToBytes("ffff80"), -65535, maxScriptNumLen, true, nil}, + {hexToBytes("000008"), 524288, maxScriptNumLen, true, nil}, + {hexToBytes("000088"), -524288, maxScriptNumLen, true, nil}, + {hexToBytes("000070"), 7340032, maxScriptNumLen, true, nil}, + {hexToBytes("0000f0"), -7340032, maxScriptNumLen, true, nil}, + {hexToBytes("00008000"), 8388608, maxScriptNumLen, true, nil}, + {hexToBytes("00008080"), -8388608, maxScriptNumLen, true, nil}, + {hexToBytes("ffffff7f"), 2147483647, maxScriptNumLen, true, nil}, + {hexToBytes("ffffffff"), -2147483647, maxScriptNumLen, true, nil}, + {hexToBytes("ffffffff7f"), 549755813887, 5, true, nil}, + {hexToBytes("ffffffffff"), -549755813887, 5, true, nil}, + {hexToBytes("ffffffffffffff7f"), 9223372036854775807, 8, true, nil}, + {hexToBytes("ffffffffffffffff"), -9223372036854775807, 8, true, nil}, + {hexToBytes("ffffffffffffffff7f"), -1, 9, true, nil}, + {hexToBytes("ffffffffffffffffff"), 1, 9, true, nil}, + {hexToBytes("ffffffffffffffffff7f"), -1, 10, true, nil}, + {hexToBytes("ffffffffffffffffffff"), 1, 10, true, nil}, + + // Minimally encoded values that are out of range for data that + // is interpreted as script numbers with the minimal encoding + // flag set. Should error and return 0. + {hexToBytes("0000008000"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000008080"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000009000"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000009080"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffff00"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffff80"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000000001"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000000081"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffff00"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffff80"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffff00"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffff80"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffff7f"), 0, maxScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffffff"), 0, maxScriptNumLen, true, errNumTooBig}, + + // Non-minimally encoded, but otherwise valid values with + // minimal encoding flag. Should error and return 0. + {hexToBytes("00"), 0, maxScriptNumLen, true, errMinimalData}, // 0 + {hexToBytes("0100"), 0, maxScriptNumLen, true, errMinimalData}, // 1 + {hexToBytes("7f00"), 0, maxScriptNumLen, true, errMinimalData}, // 127 + {hexToBytes("800000"), 0, maxScriptNumLen, true, errMinimalData}, // 128 + {hexToBytes("810000"), 0, maxScriptNumLen, true, errMinimalData}, // 129 + {hexToBytes("000100"), 0, maxScriptNumLen, true, errMinimalData}, // 256 + {hexToBytes("ff7f00"), 0, maxScriptNumLen, true, errMinimalData}, // 32767 + {hexToBytes("00800000"), 0, maxScriptNumLen, true, errMinimalData}, // 32768 + {hexToBytes("ffff0000"), 0, maxScriptNumLen, true, errMinimalData}, // 65535 + {hexToBytes("00000800"), 0, maxScriptNumLen, true, errMinimalData}, // 524288 + {hexToBytes("00007000"), 0, maxScriptNumLen, true, errMinimalData}, // 7340032 + {hexToBytes("0009000100"), 0, 5, true, errMinimalData}, // 16779520 + + // Non-minimally encoded, but otherwise valid values without + // minimal encoding flag. Should not error and return expected + // integral number. + {hexToBytes("00"), 0, maxScriptNumLen, false, nil}, + {hexToBytes("0100"), 1, maxScriptNumLen, false, nil}, + {hexToBytes("7f00"), 127, maxScriptNumLen, false, nil}, + {hexToBytes("800000"), 128, maxScriptNumLen, false, nil}, + {hexToBytes("810000"), 129, maxScriptNumLen, false, nil}, + {hexToBytes("000100"), 256, maxScriptNumLen, false, nil}, + {hexToBytes("ff7f00"), 32767, maxScriptNumLen, false, nil}, + {hexToBytes("00800000"), 32768, maxScriptNumLen, false, nil}, + {hexToBytes("ffff0000"), 65535, maxScriptNumLen, false, nil}, + {hexToBytes("00000800"), 524288, maxScriptNumLen, false, nil}, + {hexToBytes("00007000"), 7340032, maxScriptNumLen, false, nil}, + {hexToBytes("0009000100"), 16779520, 5, false, nil}, + } + + for _, test := range tests { + // Ensure the error code is of the expected type and the error + // code matches the value specified in the test instance. + gotNum, err := MakeScriptNum(test.serialized, test.minimalEncoding, + test.numLen) + if e := tstCheckScriptError(err, test.err); e != nil { + t.Errorf("MakeScriptNum(%#x): %v", test.serialized, e) + continue + } + + if gotNum != test.num { + t.Errorf("MakeScriptNum(%#x): did not get expected "+ + "number - got %d, want %d", test.serialized, + gotNum, test.num) + continue + } + } +} + +// TestScriptNumInt32 ensures that the Int32 function on script number behaves +// as expected. +func TestScriptNumInt32(t *testing.T) { + t.Parallel() + + tests := []struct { + in scriptNum + want int32 + }{ + // Values inside the valid int32 range are just the values + // themselves cast to an int32. + {0, 0}, + {1, 1}, + {-1, -1}, + {127, 127}, + {-127, -127}, + {128, 128}, + {-128, -128}, + {129, 129}, + {-129, -129}, + {256, 256}, + {-256, -256}, + {32767, 32767}, + {-32767, -32767}, + {32768, 32768}, + {-32768, -32768}, + {65535, 65535}, + {-65535, -65535}, + {524288, 524288}, + {-524288, -524288}, + {7340032, 7340032}, + {-7340032, -7340032}, + {8388608, 8388608}, + {-8388608, -8388608}, + {2147483647, 2147483647}, + {-2147483647, -2147483647}, + {-2147483648, -2147483648}, + + // Values outside of the valid int32 range are limited to int32. + {2147483648, 2147483647}, + {-2147483649, -2147483648}, + {1152921504606846975, 2147483647}, + {-1152921504606846975, -2147483648}, + {2305843009213693951, 2147483647}, + {-2305843009213693951, -2147483648}, + {4611686018427387903, 2147483647}, + {-4611686018427387903, -2147483648}, + {9223372036854775807, 2147483647}, + {-9223372036854775808, -2147483648}, + } + + for _, test := range tests { + got := test.in.Int32() + if got != test.want { + t.Errorf("Int32: did not get expected value for %d - "+ + "got %d, want %d", test.in, got, test.want) + continue + } + } +} diff --git a/pkg/ark-lib/arkade/sigvalidate.go b/pkg/ark-lib/arkade/sigvalidate.go new file mode 100644 index 000000000..faeeb3795 --- /dev/null +++ b/pkg/ark-lib/arkade/sigvalidate.go @@ -0,0 +1,479 @@ +package arkade + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +// signatureVerifier is an abstract interface that allows the op code execution +// to abstract over the _type_ of signature validation being executed. At this +// point in Bitcoin's history, there're four possible sig validation contexts: +// pre-segwit, segwit v0, segwit v1 (taproot key spend validation), and the +// base tapscript verification. +type signatureVerifier interface { + // Verify returns whether or not the signature verifier context deems the + // signature to be valid for the given context. + Verify() verifyResult +} + +type verifyResult struct { + sigValid bool + sigMatch bool +} + +// baseSigVerifier is used to verify signatures for the _base_ system, meaning +// ECDSA signatures encoded in DER or BER encoding. +type baseSigVerifier struct { + vm *Engine + + pubKey *btcec.PublicKey + + sig *ecdsa.Signature + + fullSigBytes []byte + + sigBytes []byte + pkBytes []byte + + subScript []byte + + hashType txscript.SigHashType +} + +// parseBaseSigAndPubkey attempts to parse a signature and public key according +// to the base consensus rules, which expect an 33-byte public key and DER or +// BER encoded signature. +func parseBaseSigAndPubkey(pkBytes, fullSigBytes []byte, + vm *Engine) (*btcec.PublicKey, *ecdsa.Signature, txscript.SigHashType, error) { + + strictEncoding := vm.hasFlag(txscript.ScriptVerifyStrictEncoding) || + vm.hasFlag(txscript.ScriptVerifyDERSignatures) + + // Trim off hashtype from the signature string and check if the + // signature and pubkey conform to the strict encoding requirements + // depending on the flags. + // + // NOTE: When the strict encoding flags are set, any errors in the + // signature or public encoding here result in an immediate script error + // (and thus no result bool is pushed to the data stack). This differs + // from the logic below where any errors in parsing the signature is + // treated as the signature failure resulting in false being pushed to + // the data stack. This is required because the more general script + // validation consensus rules do not have the new strict encoding + // requirements enabled by the flags. + hashType := txscript.SigHashType(fullSigBytes[len(fullSigBytes)-1]) + sigBytes := fullSigBytes[:len(fullSigBytes)-1] + if err := vm.checkHashTypeEncoding(hashType); err != nil { + return nil, nil, 0, err + } + if err := vm.checkSignatureEncoding(sigBytes); err != nil { + return nil, nil, 0, err + } + if err := vm.checkPubKeyEncoding(pkBytes); err != nil { + return nil, nil, 0, err + } + + // First, parse the public key, which we expect to be in the proper + // encoding. + pubKey, err := btcec.ParsePubKey(pkBytes) + if err != nil { + return nil, nil, 0, err + } + + // Next, parse the signature which should be in DER or BER depending on + // the active script flags. + var signature *ecdsa.Signature + if strictEncoding { + signature, err = ecdsa.ParseDERSignature(sigBytes) + } else { + signature, err = ecdsa.ParseSignature(sigBytes) + } + if err != nil { + return nil, nil, 0, err + } + + return pubKey, signature, hashType, nil +} + +// newBaseSigVerifier returns a new instance of the base signature verifier. An +// error is returned if the signature, sighash, or public key aren't correctly +// encoded. +func newBaseSigVerifier(pkBytes, fullSigBytes []byte, + vm *Engine) (*baseSigVerifier, error) { + + pubKey, sig, hashType, err := parseBaseSigAndPubkey( + pkBytes, fullSigBytes, vm, + ) + if err != nil { + return nil, err + } + + // Get script starting from the most recent OP_CODESEPARATOR. + subScript := vm.subScript() + + return &baseSigVerifier{ + vm: vm, + pubKey: pubKey, + pkBytes: pkBytes, + sig: sig, + sigBytes: fullSigBytes[:len(fullSigBytes)-1], + subScript: subScript, + hashType: hashType, + fullSigBytes: fullSigBytes, + }, nil +} + +// verifySig attempts to verify the signature given the computed sighash. A nil +// error is returned if the signature is valid. +func (b *baseSigVerifier) verifySig(sigHash []byte) bool { + var valid bool + if b.vm.sigCache != nil { + var sigHashBytes chainhash.Hash + copy(sigHashBytes[:], sigHash[:]) + + valid = b.vm.sigCache.Exists(sigHashBytes, b.sigBytes, b.pkBytes) + if !valid && b.sig.Verify(sigHash, b.pubKey) { + b.vm.sigCache.Add(sigHashBytes, b.sigBytes, b.pkBytes) + valid = true + } + } else { + valid = b.sig.Verify(sigHash, b.pubKey) + } + + return valid +} + +// Verify returns whether or not the signature verifier context deems the +// signature to be valid for the given context. +// +// NOTE: This is part of the baseSigVerifier interface. +func (b *baseSigVerifier) Verify() verifyResult { + // Remove the signature since there is no way for a signature + // to sign itself. + subScript, match := removeOpcodeByData(b.subScript, b.fullSigBytes) + + sigHash, err := txscript.CalcSignatureHash( + subScript, b.hashType, &b.vm.tx, b.vm.txIdx, + ) + if err != nil { + return verifyResult{} + } + + return verifyResult{ + sigValid: b.verifySig(sigHash), + sigMatch: match, + } +} + +// A compile-time assertion to ensure baseSigVerifier implements the +// signatureVerifier interface. +var _ signatureVerifier = (*baseSigVerifier)(nil) + +// baseSegwitSigVerifier implements signature verification for segwit v0. The +// only difference between this and the baseSigVerifier is how the sighash is +// computed. +type baseSegwitSigVerifier struct { + *baseSigVerifier +} + +// newBaseSegwitSigVerifier returns a new instance of the base segwit verifier. +func newBaseSegwitSigVerifier(pkBytes, fullSigBytes []byte, + vm *Engine) (*baseSegwitSigVerifier, error) { + + sigVerifier, err := newBaseSigVerifier(pkBytes, fullSigBytes, vm) + if err != nil { + return nil, err + } + + return &baseSegwitSigVerifier{ + baseSigVerifier: sigVerifier, + }, nil +} + +// Verify returns true if the signature verifier context deems the signature to +// be valid for the given context. +// +// NOTE: This is part of the baseSigVerifier interface. +func (s *baseSegwitSigVerifier) Verify() verifyResult { + var sigHashes *txscript.TxSigHashes + if s.vm.hashCache != nil { + sigHashes = s.vm.hashCache + } else { + sigHashes = txscript.NewTxSigHashes(&s.vm.tx, s.vm.prevOutFetcher) + } + + sigHash, err := txscript.CalcWitnessSigHash( + s.subScript, sigHashes, s.hashType, &s.vm.tx, s.vm.txIdx, + s.vm.inputAmount, + ) + if err != nil { + // TODO(roasbeef): this doesn't need to return an error, should + // instead be further up the stack? this only returns an error + // if the input index is greater than the number of inputs + return verifyResult{} + } + + return verifyResult{ + sigValid: s.verifySig(sigHash), + } +} + +// A compile-time assertion to ensure baseSegwitSigVerifier implements the +// signatureVerifier interface. +var _ signatureVerifier = (*baseSegwitSigVerifier)(nil) + +// taprootSigVerifier verifies signatures according to the segwit v1 rules, +// which are described in BIP 341. +type taprootSigVerifier struct { + pubKey *btcec.PublicKey + pkBytes []byte + + fullSigBytes []byte + sig *schnorr.Signature + + hashType txscript.SigHashType + + sigCache *txscript.SigCache + hashCache *txscript.TxSigHashes + + tx *wire.MsgTx + + inputIndex int + + annex []byte + + prevOuts txscript.PrevOutputFetcher +} + +// parseTaprootSigAndPubKey attempts to parse the public key and signature for +// a taproot spend that may be a keyspend or script path spend. This function +// returns an error if the pubkey is invalid, or the sig is. +func parseTaprootSigAndPubKey(pkBytes, rawSig []byte, +) (*btcec.PublicKey, *schnorr.Signature, txscript.SigHashType, error) { + + // Now that we have the raw key, we'll parse it into a schnorr public + // key we can work with. + pubKey, err := schnorr.ParsePubKey(pkBytes) + if err != nil { + return nil, nil, 0, err + } + + // Next, we'll parse the signature, which may or may not be appended + // with the desired sighash flag. + var ( + sig *schnorr.Signature + sigHashType txscript.SigHashType + ) + switch { + // If the signature is exactly 64 bytes, then we know we're using the + // implicit SIGHASH_DEFAULT sighash type. + case len(rawSig) == schnorr.SignatureSize: + // First, parse out the signature which is just the raw sig itself. + sig, err = schnorr.ParseSignature(rawSig) + if err != nil { + return nil, nil, 0, err + } + + // If the sig is 64 bytes, then we'll assume that it's the + // default sighash type, which is actually an alias for + // SIGHASH_ALL. + sigHashType = txscript.SigHashDefault + + // Otherwise, if this is a signature, with a sighash looking byte + // appended that isn't all zero, then we'll extract the sighash from + // the end of the signature. + case len(rawSig) == schnorr.SignatureSize+1 && rawSig[64] != 0: + // Extract the sighash type, then snip off the last byte so we can + // parse the signature. + sigHashType = txscript.SigHashType(rawSig[schnorr.SignatureSize]) + + rawSig = rawSig[:schnorr.SignatureSize] + sig, err = schnorr.ParseSignature(rawSig) + if err != nil { + return nil, nil, 0, err + } + + // Otherwise, this is an invalid signature, so we need to bail out. + default: + str := fmt.Sprintf("invalid sig len: %v", len(rawSig)) + return nil, nil, 0, scriptError(txscript.ErrInvalidTaprootSigLen, str) + } + + return pubKey, sig, sigHashType, nil +} + +// newTaprootSigVerifier returns a new instance of a taproot sig verifier given +// the necessary contextual information. +func newTaprootSigVerifier(pkBytes []byte, fullSigBytes []byte, + tx *wire.MsgTx, inputIndex int, prevOuts txscript.PrevOutputFetcher, + sigCache *txscript.SigCache, hashCache *txscript.TxSigHashes, + annex []byte) (*taprootSigVerifier, error) { + + pubKey, sig, sigHashType, err := parseTaprootSigAndPubKey( + pkBytes, fullSigBytes, + ) + if err != nil { + return nil, err + } + + return &taprootSigVerifier{ + pubKey: pubKey, + pkBytes: pkBytes, + sig: sig, + fullSigBytes: fullSigBytes, + hashType: sigHashType, + tx: tx, + inputIndex: inputIndex, + prevOuts: prevOuts, + sigCache: sigCache, + hashCache: hashCache, + annex: annex, + }, nil +} + +// verifySig attempts to verify a BIP 340 signature using the internal public +// key and signature, and the passed sigHash as the message digest. +func (t *taprootSigVerifier) verifySig(sigHash []byte) bool { + // At this point, we can check to see if this signature is already + // included in the sigCache and is valid or not (if one was passed in). + cacheKey, _ := chainhash.NewHash(sigHash) + if t.sigCache != nil { + if t.sigCache.Exists(*cacheKey, t.fullSigBytes, t.pkBytes) { + return true + } + } + + // If we didn't find the entry in the cache, then we'll perform full + // verification as normal, adding the entry to the cache if it's found + // to be valid. + sigValid := t.sig.Verify(sigHash, t.pubKey) + if sigValid { + if t.sigCache != nil { + // The sig is valid, so we'll add it to the cache. + t.sigCache.Add(*cacheKey, t.fullSigBytes, t.pkBytes) + } + + return true + } + + // Otherwise the sig is invalid if we get to this point. + return false +} + +// Verify returns whether or not the signature verifier context deems the +// signature to be valid for the given context. +// +// NOTE: This is part of the baseSigVerifier interface. +func (t *taprootSigVerifier) Verify() verifyResult { + // Before we attempt to verify the signature, we'll need to first + // compute the sighash based on the input and tx information. + sigHash, err := txscript.CalcTaprootSignatureHash( + t.hashCache, t.hashType, t.tx, t.inputIndex, t.prevOuts, + ) + if err != nil { + // TODO(roasbeef): propagate the error here? + return verifyResult{} + } + + return verifyResult{ + sigValid: t.verifySig(sigHash), + } +} + +// A compile-time assertion to ensure taprootSigVerifier implements the +// signatureVerifier interface. +var _ signatureVerifier = (*taprootSigVerifier)(nil) + +// baseTapscriptSigVerifier verifies a signature for an input spending a +// tapscript leaf from the previous output. +type baseTapscriptSigVerifier struct { + *taprootSigVerifier + + vm *Engine +} + +// newBaseTapscriptSigVerifier returns a new sig verifier for tapscript input +// spends. If the public key or signature aren't correctly formatted, an error +// is returned. +func newBaseTapscriptSigVerifier(pkBytes, rawSig []byte, + vm *Engine) (*baseTapscriptSigVerifier, error) { + + switch len(pkBytes) { + // If the public key is zero bytes, then this is invalid, and will fail + // immediately. + case 0: + return nil, scriptError(txscript.ErrTaprootPubkeyIsEmpty, "") + + // If the public key is 32 byte as we expect, then we'll parse things + // as normal. + case 32: + baseTaprootVerifier, err := newTaprootSigVerifier( + pkBytes, rawSig, &vm.tx, vm.txIdx, vm.prevOutFetcher, + vm.sigCache, vm.hashCache, vm.taprootCtx.annex, + ) + if err != nil { + return nil, err + } + + return &baseTapscriptSigVerifier{ + taprootSigVerifier: baseTaprootVerifier, + vm: vm, + }, nil + + // Otherwise, we consider this to be an unknown public key, which means + // that we'll just assume the sig to be valid. + default: + // However, if the flag preventing usage of unknown key types + // is active, then we'll return that error. + if vm.hasFlag(txscript.ScriptVerifyDiscourageUpgradeablePubkeyType) { + str := fmt.Sprintf("pubkey of length %v was used", + len(pkBytes)) + return nil, scriptError( + txscript.ErrDiscourageUpgradeablePubKeyType, str, + ) + } + + return &baseTapscriptSigVerifier{ + taprootSigVerifier: &taprootSigVerifier{}, + }, nil + } +} + +// Verify returns whether or not the signature verifier context deems the +// signature to be valid for the given context. +// +// NOTE: This is part of the baseSigVerifier interface. +func (b *baseTapscriptSigVerifier) Verify() verifyResult { + // If the public key is blank, then that means it wasn't 0 or 32 bytes, + // so we'll treat this as an unknown public key version and return + // that it's valid. + if b.pubKey == nil { + return verifyResult{ + sigValid: true, + } + } + + // Otherwise, we'll compute the sighash using the tapscript message + // extensions and return the outcome. + sigHash, err := txscript.CalcTaprootSignatureHash( + b.hashCache, b.hashType, b.tx, b.inputIndex, b.prevOuts, + ) + if err != nil { + // TODO(roasbeef): propagate the error here? + return verifyResult{} + } + + return verifyResult{ + sigValid: b.verifySig(sigHash), + } +} + +// A compile-time assertion to ensure baseTapscriptSigVerifier implements the +// signatureVerifier interface. +var _ signatureVerifier = (*baseTapscriptSigVerifier)(nil) diff --git a/pkg/ark-lib/arkade/stack.go b/pkg/ark-lib/arkade/stack.go new file mode 100644 index 000000000..2707a9393 --- /dev/null +++ b/pkg/ark-lib/arkade/stack.go @@ -0,0 +1,359 @@ +package arkade + +import ( + "encoding/hex" + "fmt" + + "github.com/btcsuite/btcd/txscript" +) + +// asBool gets the boolean value of the byte array. +func asBool(t []byte) bool { + for i := range t { + if t[i] != 0 { + // Negative 0 is also considered false. + if i == len(t)-1 && t[i] == 0x80 { + return false + } + return true + } + } + return false +} + +// fromBool converts a boolean into the appropriate byte array. +func fromBool(v bool) []byte { + if v { + return []byte{1} + } + return nil +} + +// stack represents a stack of immutable objects to be used with bitcoin +// scripts. Objects may be shared, therefore in usage if a value is to be +// changed it *must* be deep-copied first to avoid changing other values on the +// stack. +type stack struct { + stk [][]byte + verifyMinimalData bool +} + +// Depth returns the number of items on the stack. +func (s *stack) Depth() int32 { + return int32(len(s.stk)) +} + +// PushByteArray adds the given back array to the top of the stack. +// +// Stack transformation: [... x1 x2] -> [... x1 x2 data] +func (s *stack) PushByteArray(so []byte) { + s.stk = append(s.stk, so) +} + +// PushInt converts the provided scriptNum to a suitable byte array then pushes +// it onto the top of the stack. +// +// Stack transformation: [... x1 x2] -> [... x1 x2 int] +func (s *stack) PushInt(val scriptNum) { + s.PushByteArray(val.Bytes()) +} + +// PushBool converts the provided boolean to a suitable byte array then pushes +// it onto the top of the stack. +// +// Stack transformation: [... x1 x2] -> [... x1 x2 bool] +func (s *stack) PushBool(val bool) { + s.PushByteArray(fromBool(val)) +} + +// PopByteArray pops the value off the top of the stack and returns it. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func (s *stack) PopByteArray() ([]byte, error) { + return s.nipN(0) +} + +// PopInt pops the value off the top of the stack, converts it into a script +// num, and returns it. The act of converting to a script num enforces the +// consensus rules imposed on data interpreted as numbers. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func (s *stack) PopInt() (scriptNum, error) { + so, err := s.PopByteArray() + if err != nil { + return 0, err + } + + return MakeScriptNum(so, s.verifyMinimalData, maxScriptNumLen) +} + +// PopBool pops the value off the top of the stack, converts it into a bool, and +// returns it. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func (s *stack) PopBool() (bool, error) { + so, err := s.PopByteArray() + if err != nil { + return false, err + } + + return asBool(so), nil +} + +// PeekByteArray returns the Nth item on the stack without removing it. +func (s *stack) PeekByteArray(idx int32) ([]byte, error) { + sz := int32(len(s.stk)) + if idx < 0 || idx >= sz { + str := fmt.Sprintf("index %d is invalid for stack size %d", idx, + sz) + return nil, scriptError(txscript.ErrInvalidStackOperation, str) + } + + return s.stk[sz-idx-1], nil +} + +// PeekInt returns the Nth item on the stack as a script num without removing +// it. The act of converting to a script num enforces the consensus rules +// imposed on data interpreted as numbers. +func (s *stack) PeekInt(idx int32) (scriptNum, error) { + so, err := s.PeekByteArray(idx) + if err != nil { + return 0, err + } + + return MakeScriptNum(so, s.verifyMinimalData, maxScriptNumLen) +} + +// PeekBool returns the Nth item on the stack as a bool without removing it. +func (s *stack) PeekBool(idx int32) (bool, error) { + so, err := s.PeekByteArray(idx) + if err != nil { + return false, err + } + + return asBool(so), nil +} + +// nipN is an internal function that removes the nth item on the stack and +// returns it. +// +// Stack transformation: +// nipN(0): [... x1 x2 x3] -> [... x1 x2] +// nipN(1): [... x1 x2 x3] -> [... x1 x3] +// nipN(2): [... x1 x2 x3] -> [... x2 x3] +func (s *stack) nipN(idx int32) ([]byte, error) { + sz := int32(len(s.stk)) + if idx < 0 || idx > sz-1 { + str := fmt.Sprintf("index %d is invalid for stack size %d", idx, + sz) + return nil, scriptError(txscript.ErrInvalidStackOperation, str) + } + + so := s.stk[sz-idx-1] + if idx == 0 { + s.stk = s.stk[:sz-1] + } else if idx == sz-1 { + s1 := make([][]byte, sz-1) + copy(s1, s.stk[1:]) + s.stk = s1 + } else { + s1 := s.stk[sz-idx : sz] + s.stk = s.stk[:sz-idx-1] + s.stk = append(s.stk, s1...) + } + return so, nil +} + +// NipN removes the Nth object on the stack +// +// Stack transformation: +// NipN(0): [... x1 x2 x3] -> [... x1 x2] +// NipN(1): [... x1 x2 x3] -> [... x1 x3] +// NipN(2): [... x1 x2 x3] -> [... x2 x3] +func (s *stack) NipN(idx int32) error { + _, err := s.nipN(idx) + return err +} + +// Tuck copies the item at the top of the stack and inserts it before the 2nd +// to top item. +// +// Stack transformation: [... x1 x2] -> [... x2 x1 x2] +func (s *stack) Tuck() error { + so2, err := s.PopByteArray() + if err != nil { + return err + } + so1, err := s.PopByteArray() + if err != nil { + return err + } + s.PushByteArray(so2) // stack [... x2] + s.PushByteArray(so1) // stack [... x2 x1] + s.PushByteArray(so2) // stack [... x2 x1 x2] + + return nil +} + +// DropN removes the top N items from the stack. +// +// Stack transformation: +// DropN(1): [... x1 x2] -> [... x1] +// DropN(2): [... x1 x2] -> [...] +func (s *stack) DropN(n int32) error { + if n < 1 { + str := fmt.Sprintf("attempt to drop %d items from stack", n) + return scriptError(txscript.ErrInvalidStackOperation, str) + } + + for ; n > 0; n-- { + _, err := s.PopByteArray() + if err != nil { + return err + } + } + return nil +} + +// DupN duplicates the top N items on the stack. +// +// Stack transformation: +// DupN(1): [... x1 x2] -> [... x1 x2 x2] +// DupN(2): [... x1 x2] -> [... x1 x2 x1 x2] +func (s *stack) DupN(n int32) error { + if n < 1 { + str := fmt.Sprintf("attempt to dup %d stack items", n) + return scriptError(txscript.ErrInvalidStackOperation, str) + } + + // Iteratively duplicate the value n-1 down the stack n times. + // This leaves an in-order duplicate of the top n items on the stack. + for i := n; i > 0; i-- { + so, err := s.PeekByteArray(n - 1) + if err != nil { + return err + } + s.PushByteArray(so) + } + return nil +} + +// RotN rotates the top 3N items on the stack to the left N times. +// +// Stack transformation: +// RotN(1): [... x1 x2 x3] -> [... x2 x3 x1] +// RotN(2): [... x1 x2 x3 x4 x5 x6] -> [... x3 x4 x5 x6 x1 x2] +func (s *stack) RotN(n int32) error { + if n < 1 { + str := fmt.Sprintf("attempt to rotate %d stack items", n) + return scriptError(txscript.ErrInvalidStackOperation, str) + } + + // Nip the 3n-1th item from the stack to the top n times to rotate + // them up to the head of the stack. + entry := 3*n - 1 + for i := n; i > 0; i-- { + so, err := s.nipN(entry) + if err != nil { + return err + } + + s.PushByteArray(so) + } + return nil +} + +// SwapN swaps the top N items on the stack with those below them. +// +// Stack transformation: +// SwapN(1): [... x1 x2] -> [... x2 x1] +// SwapN(2): [... x1 x2 x3 x4] -> [... x3 x4 x1 x2] +func (s *stack) SwapN(n int32) error { + if n < 1 { + str := fmt.Sprintf("attempt to swap %d stack items", n) + return scriptError(txscript.ErrInvalidStackOperation, str) + } + + entry := 2*n - 1 + for i := n; i > 0; i-- { + // Swap 2n-1th entry to top. + so, err := s.nipN(entry) + if err != nil { + return err + } + + s.PushByteArray(so) + } + return nil +} + +// OverN copies N items N items back to the top of the stack. +// +// Stack transformation: +// OverN(1): [... x1 x2 x3] -> [... x1 x2 x3 x2] +// OverN(2): [... x1 x2 x3 x4] -> [... x1 x2 x3 x4 x1 x2] +func (s *stack) OverN(n int32) error { + if n < 1 { + str := fmt.Sprintf("attempt to perform over on %d stack items", + n) + return scriptError(txscript.ErrInvalidStackOperation, str) + } + + // Copy 2n-1th entry to top of the stack. + entry := 2*n - 1 + for ; n > 0; n-- { + so, err := s.PeekByteArray(entry) + if err != nil { + return err + } + s.PushByteArray(so) + } + + return nil +} + +// PickN copies the item N items back in the stack to the top. +// +// Stack transformation: +// PickN(0): [x1 x2 x3] -> [x1 x2 x3 x3] +// PickN(1): [x1 x2 x3] -> [x1 x2 x3 x2] +// PickN(2): [x1 x2 x3] -> [x1 x2 x3 x1] +func (s *stack) PickN(n int32) error { + so, err := s.PeekByteArray(n) + if err != nil { + return err + } + s.PushByteArray(so) + + return nil +} + +// RollN moves the item N items back in the stack to the top. +// +// Stack transformation: +// RollN(0): [x1 x2 x3] -> [x1 x2 x3] +// RollN(1): [x1 x2 x3] -> [x1 x3 x2] +// RollN(2): [x1 x2 x3] -> [x2 x3 x1] +func (s *stack) RollN(n int32) error { + so, err := s.nipN(n) + if err != nil { + return err + } + + s.PushByteArray(so) + + return nil +} + +// String returns the stack in a readable format. +func (s *stack) String() string { + var result string + for _, stack := range s.stk { + if len(stack) == 0 { + result += "00000000 \n" + } + result += hex.Dump(stack) + } + + return result +} diff --git a/pkg/ark-lib/arkade/stack_test.go b/pkg/ark-lib/arkade/stack_test.go new file mode 100644 index 000000000..af60cad42 --- /dev/null +++ b/pkg/ark-lib/arkade/stack_test.go @@ -0,0 +1,951 @@ +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package arkade + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/btcsuite/btcd/txscript" +) + +// tstCheckScriptError ensures the type of the two passed errors are of the +// same type (either both nil or both of type Error) and their error codes +// match when not nil. +func tstCheckScriptError(gotErr, wantErr error) error { + // Ensure the error code is of the expected type and the error + // code matches the value specified in the test instance. + if reflect.TypeOf(gotErr) != reflect.TypeOf(wantErr) { + return fmt.Errorf("wrong error - got %T (%[1]v), want %T", + gotErr, wantErr) + } + if gotErr == nil { + return nil + } + + // Ensure the want error type is a script error. + werr, ok := wantErr.(txscript.Error) + if !ok { + return fmt.Errorf("unexpected test error type %T", wantErr) + } + + // Ensure the error codes match. It's safe to use a raw type assert + // here since the code above already proved they are the same type and + // the want error is a script error. + gotErrorCode := gotErr.(txscript.Error).ErrorCode + if gotErrorCode != werr.ErrorCode { + return fmt.Errorf("mismatched error code - got %v (%v), want %v", + gotErrorCode, gotErr, werr.ErrorCode) + } + + return nil +} + +// TestStack tests that all of the stack operations work as expected. +func TestStack(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + before [][]byte + operation func(*stack) error + err error + after [][]byte + }{ + { + "noop", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) error { + return nil + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {5}}, + }, + { + "peek underflow (byte)", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) error { + _, err := s.PeekByteArray(5) + return err + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "peek underflow (int)", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) error { + _, err := s.PeekInt(5) + return err + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "peek underflow (bool)", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) error { + _, err := s.PeekBool(5) + return err + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "pop", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) error { + val, err := s.PopByteArray() + if err != nil { + return err + } + if !bytes.Equal(val, []byte{5}) { + return errors.New("not equal") + } + return err + }, + nil, + [][]byte{{1}, {2}, {3}, {4}}, + }, + { + "pop everything", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) error { + for i := 0; i < 5; i++ { + _, err := s.PopByteArray() + if err != nil { + return err + } + } + return nil + }, + nil, + nil, + }, + { + "pop underflow", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) error { + for i := 0; i < 6; i++ { + _, err := s.PopByteArray() + if err != nil { + return err + } + } + return nil + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "pop bool", + [][]byte{nil}, + func(s *stack) error { + val, err := s.PopBool() + if err != nil { + return err + } + + if val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "pop bool", + [][]byte{{1}}, + func(s *stack) error { + val, err := s.PopBool() + if err != nil { + return err + } + + if !val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "pop bool", + nil, + func(s *stack) error { + _, err := s.PopBool() + return err + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "popInt 0", + [][]byte{{0x0}}, + func(s *stack) error { + v, err := s.PopInt() + if err != nil { + return err + } + if v != 0 { + return errors.New("0 != 0 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt -0", + [][]byte{{0x80}}, + func(s *stack) error { + v, err := s.PopInt() + if err != nil { + return err + } + if v != 0 { + return errors.New("-0 != 0 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt 1", + [][]byte{{0x01}}, + func(s *stack) error { + v, err := s.PopInt() + if err != nil { + return err + } + if v != 1 { + return errors.New("1 != 1 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt 1 leading 0", + [][]byte{{0x01, 0x00, 0x00, 0x00}}, + func(s *stack) error { + v, err := s.PopInt() + if err != nil { + return err + } + if v != 1 { + fmt.Printf("%v != %v\n", v, 1) + return errors.New("1 != 1 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt -1", + [][]byte{{0x81}}, + func(s *stack) error { + v, err := s.PopInt() + if err != nil { + return err + } + if v != -1 { + return errors.New("-1 != -1 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt -1 leading 0", + [][]byte{{0x01, 0x00, 0x00, 0x80}}, + func(s *stack) error { + v, err := s.PopInt() + if err != nil { + return err + } + if v != -1 { + fmt.Printf("%v != %v\n", v, -1) + return errors.New("-1 != -1 on popInt") + } + return nil + }, + nil, + nil, + }, + // Triggers the multibyte case in asInt + { + "popInt -513", + [][]byte{{0x1, 0x82}}, + func(s *stack) error { + v, err := s.PopInt() + if err != nil { + return err + } + if v != -513 { + fmt.Printf("%v != %v\n", v, -513) + return errors.New("1 != 1 on popInt") + } + return nil + }, + nil, + nil, + }, + // Confirm that the asInt code doesn't modify the base data. + { + "peekint nomodify -1", + [][]byte{{0x01, 0x00, 0x00, 0x80}}, + func(s *stack) error { + v, err := s.PeekInt(0) + if err != nil { + return err + } + if v != -1 { + fmt.Printf("%v != %v\n", v, -1) + return errors.New("-1 != -1 on popInt") + } + return nil + }, + nil, + [][]byte{{0x01, 0x00, 0x00, 0x80}}, + }, + { + "PushInt 0", + nil, + func(s *stack) error { + s.PushInt(scriptNum(0)) + return nil + }, + nil, + [][]byte{{}}, + }, + { + "PushInt 1", + nil, + func(s *stack) error { + s.PushInt(scriptNum(1)) + return nil + }, + nil, + [][]byte{{0x1}}, + }, + { + "PushInt -1", + nil, + func(s *stack) error { + s.PushInt(scriptNum(-1)) + return nil + }, + nil, + [][]byte{{0x81}}, + }, + { + "PushInt two bytes", + nil, + func(s *stack) error { + s.PushInt(scriptNum(256)) + return nil + }, + nil, + // little endian.. *sigh* + [][]byte{{0x00, 0x01}}, + }, + { + "PushInt leading zeros", + nil, + func(s *stack) error { + // this will have the highbit set + s.PushInt(scriptNum(128)) + return nil + }, + nil, + [][]byte{{0x80, 0x00}}, + }, + { + "dup", + [][]byte{{1}}, + func(s *stack) error { + return s.DupN(1) + }, + nil, + [][]byte{{1}, {1}}, + }, + { + "dup2", + [][]byte{{1}, {2}}, + func(s *stack) error { + return s.DupN(2) + }, + nil, + [][]byte{{1}, {2}, {1}, {2}}, + }, + { + "dup3", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.DupN(3) + }, + nil, + [][]byte{{1}, {2}, {3}, {1}, {2}, {3}}, + }, + { + "dup0", + [][]byte{{1}}, + func(s *stack) error { + return s.DupN(0) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "dup-1", + [][]byte{{1}}, + func(s *stack) error { + return s.DupN(-1) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "dup too much", + [][]byte{{1}}, + func(s *stack) error { + return s.DupN(2) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "PushBool true", + nil, + func(s *stack) error { + s.PushBool(true) + + return nil + }, + nil, + [][]byte{{1}}, + }, + { + "PushBool false", + nil, + func(s *stack) error { + s.PushBool(false) + + return nil + }, + nil, + [][]byte{nil}, + }, + { + "PushBool PopBool", + nil, + func(s *stack) error { + s.PushBool(true) + val, err := s.PopBool() + if err != nil { + return err + } + if !val { + return errors.New("unexpected value") + } + + return nil + }, + nil, + nil, + }, + { + "PushBool PopBool 2", + nil, + func(s *stack) error { + s.PushBool(false) + val, err := s.PopBool() + if err != nil { + return err + } + if val { + return errors.New("unexpected value") + } + + return nil + }, + nil, + nil, + }, + { + "PushInt PopBool", + nil, + func(s *stack) error { + s.PushInt(scriptNum(1)) + val, err := s.PopBool() + if err != nil { + return err + } + if !val { + return errors.New("unexpected value") + } + + return nil + }, + nil, + nil, + }, + { + "PushInt PopBool 2", + nil, + func(s *stack) error { + s.PushInt(scriptNum(0)) + val, err := s.PopBool() + if err != nil { + return err + } + if val { + return errors.New("unexpected value") + } + + return nil + }, + nil, + nil, + }, + { + "Nip top", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.NipN(0) + }, + nil, + [][]byte{{1}, {2}}, + }, + { + "Nip middle", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.NipN(1) + }, + nil, + [][]byte{{1}, {3}}, + }, + { + "Nip low", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.NipN(2) + }, + nil, + [][]byte{{2}, {3}}, + }, + { + "Nip too much", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + // bite off more than we can chew + return s.NipN(3) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + [][]byte{{2}, {3}}, + }, + { + "keep on tucking", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.Tuck() + }, + nil, + [][]byte{{1}, {3}, {2}, {3}}, + }, + { + "a little tucked up", + [][]byte{{1}}, // too few arguments for tuck + func(s *stack) error { + return s.Tuck() + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "all tucked up", + nil, // too few arguments for tuck + func(s *stack) error { + return s.Tuck() + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "drop 1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.DropN(1) + }, + nil, + [][]byte{{1}, {2}, {3}}, + }, + { + "drop 2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.DropN(2) + }, + nil, + [][]byte{{1}, {2}}, + }, + { + "drop 3", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.DropN(3) + }, + nil, + [][]byte{{1}}, + }, + { + "drop 4", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.DropN(4) + }, + nil, + nil, + }, + { + "drop 4/5", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.DropN(5) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "drop invalid", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.DropN(0) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Rot1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.RotN(1) + }, + nil, + [][]byte{{1}, {3}, {4}, {2}}, + }, + { + "Rot2", + [][]byte{{1}, {2}, {3}, {4}, {5}, {6}}, + func(s *stack) error { + return s.RotN(2) + }, + nil, + [][]byte{{3}, {4}, {5}, {6}, {1}, {2}}, + }, + { + "Rot too little", + [][]byte{{1}, {2}}, + func(s *stack) error { + return s.RotN(1) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Rot0", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.RotN(0) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Swap1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.SwapN(1) + }, + nil, + [][]byte{{1}, {2}, {4}, {3}}, + }, + { + "Swap2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.SwapN(2) + }, + nil, + [][]byte{{3}, {4}, {1}, {2}}, + }, + { + "Swap too little", + [][]byte{{1}}, + func(s *stack) error { + return s.SwapN(1) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Swap0", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.SwapN(0) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Over1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.OverN(1) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {3}}, + }, + { + "Over2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.OverN(2) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {1}, {2}}, + }, + { + "Over too little", + [][]byte{{1}}, + func(s *stack) error { + return s.OverN(1) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Over0", + [][]byte{{1}, {2}, {3}}, + func(s *stack) error { + return s.OverN(0) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Pick1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.PickN(1) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {3}}, + }, + { + "Pick2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.PickN(2) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {2}}, + }, + { + "Pick too little", + [][]byte{{1}}, + func(s *stack) error { + return s.PickN(1) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Roll1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.RollN(1) + }, + nil, + [][]byte{{1}, {2}, {4}, {3}}, + }, + { + "Roll2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) error { + return s.RollN(2) + }, + nil, + [][]byte{{1}, {3}, {4}, {2}}, + }, + { + "Roll too little", + [][]byte{{1}}, + func(s *stack) error { + return s.RollN(1) + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + { + "Peek bool", + [][]byte{{1}}, + func(s *stack) error { + // Peek bool is otherwise pretty well tested, + // just check it works. + val, err := s.PeekBool(0) + if err != nil { + return err + } + if !val { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{{1}}, + }, + { + "Peek bool 2", + [][]byte{nil}, + func(s *stack) error { + // Peek bool is otherwise pretty well tested, + // just check it works. + val, err := s.PeekBool(0) + if err != nil { + return err + } + if val { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{nil}, + }, + { + "Peek int", + [][]byte{{1}}, + func(s *stack) error { + // Peek int is otherwise pretty well tested, + // just check it works. + val, err := s.PeekInt(0) + if err != nil { + return err + } + if val != 1 { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{{1}}, + }, + { + "Peek int 2", + [][]byte{{0}}, + func(s *stack) error { + // Peek int is otherwise pretty well tested, + // just check it works. + val, err := s.PeekInt(0) + if err != nil { + return err + } + if val != 0 { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{{0}}, + }, + { + "pop int", + nil, + func(s *stack) error { + s.PushInt(scriptNum(1)) + // Peek int is otherwise pretty well tested, + // just check it works. + val, err := s.PopInt() + if err != nil { + return err + } + if val != 1 { + return errors.New("invalid result") + } + return nil + }, + nil, + nil, + }, + { + "pop empty", + nil, + func(s *stack) error { + // Peek int is otherwise pretty well tested, + // just check it works. + _, err := s.PopInt() + return err + }, + scriptError(txscript.ErrInvalidStackOperation, ""), + nil, + }, + } + + for _, test := range tests { + // Setup the initial stack state and perform the test operation. + s := stack{} + for i := range test.before { + s.PushByteArray(test.before[i]) + } + err := test.operation(&s) + + // Ensure the error code is of the expected type and the error + // code matches the value specified in the test instance. + if e := tstCheckScriptError(err, test.err); e != nil { + t.Errorf("%s: %v", test.name, e) + continue + } + if err != nil { + continue + } + + // Ensure the resulting stack is the expected length. + if int32(len(test.after)) != s.Depth() { + t.Errorf("%s: stack depth doesn't match expected: %v "+ + "vs %v", test.name, len(test.after), + s.Depth()) + continue + } + + // Ensure all items of the resulting stack are the expected + // values. + for i := range test.after { + val, err := s.PeekByteArray(s.Depth() - int32(i) - 1) + if err != nil { + t.Errorf("%s: can't peek %dth stack entry: %v", + test.name, i, err) + break + } + + if !bytes.Equal(val, test.after[i]) { + t.Errorf("%s: %dth stack entry doesn't match "+ + "expected: %v vs %v", test.name, i, val, + test.after[i]) + break + } + } + } +} diff --git a/pkg/ark-lib/arkade/tokenizer.go b/pkg/ark-lib/arkade/tokenizer.go new file mode 100644 index 000000000..cf421e3be --- /dev/null +++ b/pkg/ark-lib/arkade/tokenizer.go @@ -0,0 +1,207 @@ +package arkade + +import ( + "encoding/binary" + "fmt" + + "github.com/btcsuite/btcd/txscript" +) + +// opcodeArrayRef is used to break initialization cycles. +var opcodeArrayRef *[256]opcode + +func init() { + opcodeArrayRef = &opcodeArray +} + +// ScriptTokenizer provides a facility for easily and efficiently tokenizing +// transaction scripts without creating allocations. Each successive opcode is +// parsed with the Next function, which returns false when iteration is +// complete, either due to successfully tokenizing the entire script or +// encountering a parse error. In the case of failure, the Err function may be +// used to obtain the specific parse error. +// +// Upon successfully parsing an opcode, the opcode and data associated with it +// may be obtained via the Opcode and Data functions, respectively. +// +// The ByteIndex function may be used to obtain the tokenizer's current offset +// into the raw script. +type ScriptTokenizer struct { + script []byte + version uint16 + offset int32 + opcodePos int32 + op *opcode + data []byte + err error +} + +// Done returns true when either all opcodes have been exhausted or a parse +// failure was encountered and therefore the state has an associated error. +func (t *ScriptTokenizer) Done() bool { + return t.err != nil || t.offset >= int32(len(t.script)) +} + +// Next attempts to parse the next opcode and returns whether or not it was +// successful. It will not be successful if invoked when already at the end of +// the script, a parse failure is encountered, or an associated error already +// exists due to a previous parse failure. +// +// In the case of a true return, the parsed opcode and data can be obtained with +// the associated functions and the offset into the script will either point to +// the next opcode or the end of the script if the final opcode was parsed. +// +// In the case of a false return, the parsed opcode and data will be the last +// successfully parsed values (if any) and the offset into the script will +// either point to the failing opcode or the end of the script if the function +// was invoked when already at the end of the script. +// +// Invoking this function when already at the end of the script is not +// considered an error and will simply return false. +func (t *ScriptTokenizer) Next() bool { + if t.Done() { + return false + } + + // Increment the op code position each time we attempt to parse the + // next op code. Note that since the starting value is -1 (no op codes + // parsed), by incrementing here, we start at 0, then 1, and so on for + // the other op codes. + t.opcodePos++ + + op := &opcodeArrayRef[t.script[t.offset]] + switch { + // No additional data. Note that some of the opcodes, notably OP_1NEGATE, + // OP_0, and OP_[1-16] represent the data themselves. + case op.length == 1: + t.offset++ + t.op = op + t.data = nil + return true + + // Data pushes of specific lengths -- OP_DATA_[1-75]. + case op.length > 1: + script := t.script[t.offset:] + if len(script) < op.length { + str := fmt.Sprintf("opcode %s requires %d bytes, but script only "+ + "has %d remaining", op.name, op.length, len(script)) + t.err = scriptError(txscript.ErrMalformedPush, str) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += int32(op.length) + t.op = op + t.data = script[1:op.length] + return true + + // Data pushes with parsed lengths -- OP_PUSHDATA{1,2,4}. + case op.length < 0: + script := t.script[t.offset+1:] + if len(script) < -op.length { + str := fmt.Sprintf("opcode %s requires %d bytes, but script only "+ + "has %d remaining", op.name, -op.length, len(script)) + t.err = scriptError(txscript.ErrMalformedPush, str) + return false + } + + // Next -length bytes are little endian length of data. + var dataLen int32 + switch op.length { + case -1: + dataLen = int32(script[0]) + case -2: + dataLen = int32(binary.LittleEndian.Uint16(script[:2])) + case -4: + dataLen = int32(binary.LittleEndian.Uint32(script[:4])) + default: + // In practice it should be impossible to hit this + // check as each op code is predefined, and only uses + // the specified lengths. + str := fmt.Sprintf("invalid opcode length %d", op.length) + t.err = scriptError(txscript.ErrMalformedPush, str) + return false + } + + // Move to the beginning of the data. + script = script[-op.length:] + + // Disallow entries that do not fit script or were sign extended. + if dataLen > int32(len(script)) || dataLen < 0 { + str := fmt.Sprintf("opcode %s pushes %d bytes, but script only "+ + "has %d remaining", op.name, dataLen, len(script)) + t.err = scriptError(txscript.ErrMalformedPush, str) + return false + } + + // Move the offset forward and set the opcode and data accordingly. + t.offset += 1 + int32(-op.length) + dataLen + t.op = op + t.data = script[:dataLen] + return true + } + + // The only remaining case is an opcode with length zero which is + // impossible. + panic("unreachable") +} + +// Script returns the full script associated with the tokenizer. +func (t *ScriptTokenizer) Script() []byte { + return t.script +} + +// ByteIndex returns the current offset into the full script that will be parsed +// next and therefore also implies everything before it has already been parsed. +func (t *ScriptTokenizer) ByteIndex() int32 { + return t.offset +} + +// OpcodePosition returns the current op code counter. Unlike the ByteIndex +// above (referred to as the program counter or pc at times), this is +// incremented with each node op code, and isn't incremented more than once for +// push datas. +// +// NOTE: If no op codes have been parsed, this returns -1. +func (t *ScriptTokenizer) OpcodePosition() int32 { + return t.opcodePos +} + +// Opcode returns the current opcode associated with the tokenizer. +func (t *ScriptTokenizer) Opcode() byte { + return t.op.value +} + +// Data returns the data associated with the most recently successfully parsed +// opcode. +func (t *ScriptTokenizer) Data() []byte { + return t.data +} + +// Err returns any errors currently associated with the tokenizer. This will +// only be non-nil in the case a parsing error was encountered. +func (t *ScriptTokenizer) Err() error { + return t.err +} + +// MakeScriptTokenizer returns a new instance of a script tokenizer. Passing +// an unsupported script version will result in the returned tokenizer +// immediately having an err set accordingly. +// +// See the docs for ScriptTokenizer for more details. +func MakeScriptTokenizer(scriptVersion uint16, script []byte) ScriptTokenizer { + // Only version 0 scripts are currently supported. + var err error + if scriptVersion != 0 { + str := fmt.Sprintf("script version %d is not supported", scriptVersion) + err = scriptError(txscript.ErrUnsupportedScriptVersion, str) + + } + return ScriptTokenizer{ + version: scriptVersion, + script: script, + err: err, + // We use a value of negative 1 here so the first op code has a value of 0. + opcodePos: -1, + } +} diff --git a/pkg/ark-lib/go.mod b/pkg/ark-lib/go.mod index 08899f423..4f0479cef 100644 --- a/pkg/ark-lib/go.mod +++ b/pkg/ark-lib/go.mod @@ -22,6 +22,7 @@ require ( github.com/lightninglabs/neutrino/cache v1.1.2 // indirect github.com/lightningnetwork/lnd/fn v1.2.1 // indirect github.com/lightningnetwork/lnd/tlv v1.2.6 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect go.etcd.io/bbolt v1.3.10 // indirect golang.org/x/crypto v0.35.0 // indirect @@ -35,4 +36,5 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/mathutil v1.7.1 ) diff --git a/pkg/ark-lib/go.sum b/pkg/ark-lib/go.sum index dbe38bb52..e83b39446 100644 --- a/pkg/ark-lib/go.sum +++ b/pkg/ark-lib/go.sum @@ -85,6 +85,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= @@ -148,3 +150,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= diff --git a/pkg/ark-lib/note/note.go b/pkg/ark-lib/note/note.go index c5c2637e3..318559850 100644 --- a/pkg/ark-lib/note/note.go +++ b/pkg/ark-lib/note/note.go @@ -179,16 +179,21 @@ func (n *NoteClosure) Decode(script []byte) (bool, error) { } // Witness returns the witness stack for spending the fake vtxo note -func (n *NoteClosure) Witness(controlBlock []byte, opts map[string][]byte) (wire.TxWitness, error) { +func (n *NoteClosure) Witness(controlBlock []byte, opts map[string]any) (wire.TxWitness, error) { preimage, ok := opts["preimage"] if !ok { return nil, fmt.Errorf("missing preimage for hash %x", n.PreimageHash) } + preimageBytes, ok := preimage.([]byte) + if !ok { + return nil, fmt.Errorf("invalid preimage type: %T", preimage) + } + script, err := n.Script() if err != nil { return nil, fmt.Errorf("failed to generate script: %w", err) } - return wire.TxWitness{preimage, script, controlBlock}, nil + return wire.TxWitness{preimageBytes, script, controlBlock}, nil } diff --git a/pkg/ark-lib/offchain/tx.go b/pkg/ark-lib/offchain/tx.go index bef39a3b2..98fa2e361 100644 --- a/pkg/ark-lib/offchain/tx.go +++ b/pkg/ark-lib/offchain/tx.go @@ -1,6 +1,7 @@ package offchain import ( + "bytes" "fmt" common "github.com/arkade-os/arkd/pkg/ark-lib" @@ -27,12 +28,33 @@ type VtxoInput struct { // it can be nil, defaulting to Tapscript if not set CheckpointTapscript *waddrmgr.Tapscript RevealedTapscripts []string + + ArkadeScript []byte +} + +func (v *VtxoInput) Validate() error { + if len(v.ArkadeScript) > 0 && v.CheckpointTapscript != nil { + if !bytes.Equal(v.CheckpointTapscript.RevealedScript, v.Tapscript.RevealedScript) { + return fmt.Errorf( + "invalid input %s, arkade script and checkpoint tapscript cannot be set at the same time", + v.Outpoint.String(), + ) + } + } + + return nil } // BuildTxs builds the ark and checkpoint txs for the given inputs and outputs. func BuildTxs( vtxos []VtxoInput, outputs []*wire.TxOut, signerUnrollScript *script.CSVMultisigClosure, ) (*psbt.Packet, []*psbt.Packet, error) { + for _, vtxo := range vtxos { + if err := vtxo.Validate(); err != nil { + return nil, nil, err + } + } + checkpointInputs := make([]VtxoInput, 0, len(vtxos)) checkpointTxs := make([]*psbt.Packet, 0, len(vtxos)) inputAmount := int64(0) @@ -157,6 +179,10 @@ func buildArkTx(vtxos []VtxoInput, outputs []*wire.TxOut) (*psbt.Packet, error) if err := txutils.AddTaprootTree(i, arkTx, tapscripts[i]); err != nil { return nil, err } + + if len(vtxos[i].ArkadeScript) > 0 { + txutils.AddArkScript(i, arkTx, vtxos[i].ArkadeScript) + } } return arkTx, nil @@ -233,6 +259,7 @@ func buildCheckpointTx( RevealedScript: collaborativeLeafProof.Script, }, RevealedTapscripts: revealedTapscripts, + ArkadeScript: vtxo.ArkadeScript, } return checkpointPtx, checkpointInput, nil diff --git a/pkg/ark-lib/script/closure.go b/pkg/ark-lib/script/closure.go index 69b426d8a..d36f72706 100644 --- a/pkg/ark-lib/script/closure.go +++ b/pkg/ark-lib/script/closure.go @@ -7,17 +7,59 @@ import ( "strings" arklib "github.com/arkade-os/arkd/pkg/ark-lib" + "github.com/arkade-os/arkd/pkg/ark-lib/arkade" "github.com/arkade-os/arkd/pkg/ark-lib/txutils" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" ) +var ( + TagArkScriptHash = []byte("ArkScriptHash") +) + +const ( + OP_INSPECTOUTPUTSCRIPTPUBKEY = 0xd1 + OP_INSPECTOUTPUTVALUE = 0xcf + OP_PUSHCURRENTINPUTINDEX = 0xcd + OP_INSPECTINPUTVALUE = 0xc9 + OP_SUB64 = 0xd8 +) + +type MultisigType int + +const ( + MultisigTypeChecksig MultisigType = iota + MultisigTypeChecksigAdd +) + +const ( + ConditionWitnessKey = "condition" + SpendingTxKey = "spending_tx" + InputIndexKey = "input_index" + PrevoutFetcherKey = "prevout_fetcher" + ArkScriptStackKey = "arkade_script_stack" + ArkScriptKey = "arkade_script" +) + +// forbiddenOpcodes are opcodes that are not allowed in a condition script +var forbiddenOpcodes = []byte{ + txscript.OP_CHECKMULTISIG, + txscript.OP_CHECKSIG, + txscript.OP_CHECKSIGVERIFY, + txscript.OP_CHECKSIGADD, + txscript.OP_CHECKMULTISIGVERIFY, + txscript.OP_CHECKLOCKTIMEVERIFY, + txscript.OP_CHECKSEQUENCEVERIFY, +} + type Closure interface { Script() ([]byte, error) Decode(script []byte) (bool, error) - Witness(controlBlock []byte, opts map[string][]byte) (wire.TxWitness, error) + Witness(controlBlock []byte, args map[string]any) (wire.TxWitness, error) } // MultisigClosure is a closure that contains a list of public keys and a @@ -55,6 +97,8 @@ func DecodeClosure(script []byte) (Closure, error) { } if valid { return t.closure, nil + } else { + decodeErr = append(decodeErr, fmt.Sprintf("%s: %v", t.name, "invalid")) } } @@ -254,19 +298,16 @@ func (f *MultisigClosure) decodeChecksig(script []byte) (bool, error) { return true, nil } -func (f *MultisigClosure) Witness( - controlBlock []byte, signatures map[string][]byte, -) (wire.TxWitness, error) { +func (f *MultisigClosure) Witness(controlBlock []byte, args map[string]interface{}) (wire.TxWitness, error) { // Create witness stack with capacity for all signatures plus script and control block witness := make(wire.TxWitness, 0, len(f.PubKeys)+2) // Add signatures in the reverse order as public keys for i := len(f.PubKeys) - 1; i >= 0; i-- { pubkey := f.PubKeys[i] - xOnlyPubkey := schnorr.SerializePubKey(pubkey) - sig, ok := signatures[hex.EncodeToString(xOnlyPubkey)] + sig, ok := args[hex.EncodeToString(schnorr.SerializePubKey(pubkey))].([]byte) if !ok { - return nil, fmt.Errorf("missing signature for pubkey %x", xOnlyPubkey) + return nil, fmt.Errorf("missing signature for public key %x", schnorr.SerializePubKey(pubkey)) } witness = append(witness, sig) } @@ -293,7 +334,7 @@ type CSVMultisigClosure struct { } func (f *CSVMultisigClosure) Witness( - controlBlock []byte, signatures map[string][]byte, + controlBlock []byte, signatures map[string]any, ) (wire.TxWitness, error) { multisigWitness, err := f.MultisigClosure.Witness(controlBlock, signatures) if err != nil { @@ -396,7 +437,7 @@ type CLTVMultisigClosure struct { } func (f *CLTVMultisigClosure) Witness( - controlBlock []byte, signatures map[string][]byte, + controlBlock []byte, signatures map[string]any, ) (wire.TxWitness, error) { multisigWitness, err := f.MultisigClosure.Witness(controlBlock, signatures) if err != nil { @@ -548,15 +589,20 @@ func (f *ConditionMultisigClosure) Decode(script []byte) (bool, error) { } func (f *ConditionMultisigClosure) Witness( - controlBlock []byte, args map[string][]byte, + controlBlock []byte, args map[string]any, ) (wire.TxWitness, error) { script, err := f.Script() if err != nil { return nil, fmt.Errorf("failed to generate script: %w", err) } + condWitnessBytes, ok := args[ConditionWitnessKey].([]byte) + if !ok { + return nil, fmt.Errorf("missing or invalid condition witness") + } + // Read and execute condition witness - condWitness, err := txutils.ReadTxWitness(args[ConditionWitnessKey]) + condWitness, err := txutils.ReadTxWitness(condWitnessBytes) if err != nil { return nil, fmt.Errorf("failed to read condition witness: %w", err) } @@ -646,7 +692,7 @@ func (f *ConditionCSVMultisigClosure) Decode(script []byte) (bool, error) { } func (f *ConditionCSVMultisigClosure) Witness( - controlBlock []byte, args map[string][]byte, + controlBlock []byte, args map[string]any, ) (wire.TxWitness, error) { script, err := f.Script() if err != nil { @@ -654,7 +700,12 @@ func (f *ConditionCSVMultisigClosure) Witness( } // Read and execute condition witness - condWitness, err := txutils.ReadTxWitness(args[ConditionWitnessKey]) + condWitnessBytes, ok := args[ConditionWitnessKey].([]byte) + if !ok { + return nil, fmt.Errorf("missing or invalid condition witness") + } + + condWitness, err := txutils.ReadTxWitness(condWitnessBytes) if err != nil { return nil, fmt.Errorf("failed to read condition witness: %w", err) } @@ -681,3 +732,76 @@ func (f *ConditionCSVMultisigClosure) Witness( return witness, nil } + +func ExecuteArkadeScript( + arkScript []byte, + spendingTx *wire.MsgTx, + prevoutFetcher txscript.PrevOutputFetcher, + inputIndex int, + arkScriptStack wire.TxWitness, +) error { + engine, err := arkade.NewEngine( + arkScript, + spendingTx, + inputIndex, + txscript.StandardVerifyFlags, + txscript.NewSigCache(100), + txscript.NewTxSigHashes(spendingTx, prevoutFetcher), + 0, // TODO : add input amount if need CHECKSIG in custom script? + prevoutFetcher, + ) + if err != nil { + return fmt.Errorf("failed to create engine: %w", err) + } + + if len(arkScriptStack) > 0 { + engine.SetStack(arkScriptStack) + } + + if err := engine.Execute(); err != nil { + return fmt.Errorf("failed to execute custom script: %w", err) + } + + return nil +} + +// ArkadeScriptHash computes the hash of an ark script. +// scripthash = h_arkScriptHash(script) +func ArkadeScriptHash(script []byte) []byte { + hash := chainhash.TaggedHash(TagArkScriptHash, script) + return hash[:] +} + +// ComputeArkadeScriptPublicKey calculates a top-level ark script public key given the hash of the arkscript +func ComputeArkadeScriptPublicKey(pubKey *btcec.PublicKey, scriptHash []byte) *btcec.PublicKey { + pubKey, _ = schnorr.ParsePubKey(schnorr.SerializePubKey(pubKey)) + + var ( + pubKeyJacobian btcec.JacobianPoint + tweakJacobian btcec.JacobianPoint + resultJacobian btcec.JacobianPoint + ) + tweakKey, _ := btcec.PrivKeyFromBytes(scriptHash) + btcec.ScalarBaseMultNonConst(&tweakKey.Key, &tweakJacobian) + + pubKey.AsJacobian(&pubKeyJacobian) + btcec.AddNonConst(&pubKeyJacobian, &tweakJacobian, &resultJacobian) + + resultJacobian.ToAffine() + return btcec.NewPublicKey(&resultJacobian.X, &resultJacobian.Y) +} + +func ComputeArkadeScriptPrivateKey(privKey *btcec.PrivateKey, scriptHash []byte) *btcec.PrivateKey { + privKeyScalar := privKey.Key + pubKeyBytes := privKey.PubKey().SerializeCompressed() + if pubKeyBytes[0] == 0x03 { + privKeyScalar.Negate() + } + + tweakScalar := new(btcec.ModNScalar) + tweakScalar.SetByteSlice(scriptHash) + + tweakScalar.Add(&privKeyScalar) + + return &btcec.PrivateKey{Key: *tweakScalar} +} diff --git a/pkg/ark-lib/script/script.go b/pkg/ark-lib/script/script.go index 96285650c..f6b76cad6 100644 --- a/pkg/ark-lib/script/script.go +++ b/pkg/ark-lib/script/script.go @@ -9,34 +9,6 @@ import ( "github.com/btcsuite/btcd/wire" ) -const ( - OP_INSPECTOUTPUTSCRIPTPUBKEY = 0xd1 - OP_INSPECTOUTPUTVALUE = 0xcf - OP_PUSHCURRENTINPUTINDEX = 0xcd - OP_INSPECTINPUTVALUE = 0xc9 - OP_SUB64 = 0xd8 -) - -type MultisigType int - -const ( - MultisigTypeChecksig MultisigType = iota - MultisigTypeChecksigAdd -) - -var ConditionWitnessKey = "condition" - -// forbiddenOpcodes are opcodes that are not allowed in a condition script -var forbiddenOpcodes = []byte{ - txscript.OP_CHECKMULTISIG, - txscript.OP_CHECKSIG, - txscript.OP_CHECKSIGVERIFY, - txscript.OP_CHECKSIGADD, - txscript.OP_CHECKMULTISIGVERIFY, - txscript.OP_CHECKLOCKTIMEVERIFY, - txscript.OP_CHECKSEQUENCEVERIFY, -} - // EvaluateScriptToBool executes the script with the provided witness as argument and returns a // boolean result that can be evaluated by OP_IF / OP_NOIF opcodes. func EvaluateScriptToBool(script []byte, witness wire.TxWitness) (bool, error) { diff --git a/pkg/ark-lib/script/script_test.go b/pkg/ark-lib/script/script_test.go index 5d6a335d8..236034b79 100644 --- a/pkg/ark-lib/script/script_test.go +++ b/pkg/ark-lib/script/script_test.go @@ -705,7 +705,7 @@ func TestMultisigClosureWitness(t *testing.T) { testCases := []struct { name string closure *script.MultisigClosure - signatures map[string][]byte + signatures map[string]any expectError bool }{ { @@ -713,7 +713,7 @@ func TestMultisigClosureWitness(t *testing.T) { closure: &script.MultisigClosure{ PubKeys: []*btcec.PublicKey{pub1}, }, - signatures: map[string][]byte{ + signatures: map[string]any{ hex.EncodeToString(schnorr.SerializePubKey(pub1)): []byte("signature1"), }, expectError: false, @@ -723,7 +723,7 @@ func TestMultisigClosureWitness(t *testing.T) { closure: &script.MultisigClosure{ PubKeys: []*btcec.PublicKey{pub1, pub2}, }, - signatures: map[string][]byte{ + signatures: map[string]any{ hex.EncodeToString(schnorr.SerializePubKey(pub1)): []byte("signature1"), hex.EncodeToString(schnorr.SerializePubKey(pub2)): []byte("signature2"), }, @@ -734,7 +734,7 @@ func TestMultisigClosureWitness(t *testing.T) { closure: &script.MultisigClosure{ PubKeys: []*btcec.PublicKey{pub1, pub2}, }, - signatures: map[string][]byte{ + signatures: map[string]any{ hex.EncodeToString(schnorr.SerializePubKey(pub1)): []byte("signature1"), }, expectError: true, @@ -781,7 +781,7 @@ func TestCSVMultisigClosureWitness(t *testing.T) { // Create test signature testSig := []byte("signature1") - signatures := map[string][]byte{ + signatures := map[string]any{ hex.EncodeToString(schnorr.SerializePubKey(pub1)): testSig, } @@ -961,7 +961,7 @@ func TestCLTVMultisigClosure(t *testing.T) { } controlBlock := bytes.Repeat([]byte{0x00}, 32) - signatures := map[string][]byte{ + signatures := map[string]any{ hex.EncodeToString(schnorr.SerializePubKey(pubkey1)): bytes.Repeat([]byte{0x01}, 64), hex.EncodeToString(schnorr.SerializePubKey(pubkey2)): bytes.Repeat([]byte{0x01}, 64), } @@ -1222,7 +1222,7 @@ func TestConditionMultisigClosure(t *testing.T) { var conditionWitnessBytes bytes.Buffer require.NoError(t, psbt.WriteTxWitness(&conditionWitnessBytes, conditionWitness)) - signatures := map[string][]byte{ + signatures := map[string]any{ hex.EncodeToString(schnorr.SerializePubKey(pubkey1)): bytes.Repeat([]byte{0x01}, 64), hex.EncodeToString(schnorr.SerializePubKey(pubkey2)): bytes.Repeat([]byte{0x02}, 64), script.ConditionWitnessKey: conditionWitnessBytes.Bytes(), diff --git a/pkg/ark-lib/script/vtxo_script.go b/pkg/ark-lib/script/vtxo_script.go index 4cd511907..dc83113d0 100644 --- a/pkg/ark-lib/script/vtxo_script.go +++ b/pkg/ark-lib/script/vtxo_script.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "fmt" + "strings" common "github.com/arkade-os/arkd/pkg/ark-lib" "github.com/btcsuite/btcd/btcec/v2" @@ -94,10 +95,14 @@ func (v *TapscriptsVtxoScript) Decode(scripts []string) error { return nil } -func (v *TapscriptsVtxoScript) Validate( - signer *btcec.PublicKey, minLocktime common.RelativeLocktime, blockTypeAllowed bool, -) error { - xOnlySigner := schnorr.SerializePubKey(signer) +func (v *TapscriptsVtxoScript) Validate(server *btcec.PublicKey, minLocktime common.RelativeLocktime, blockTypeAllowed bool, arkScript []byte) error { + xonlySigner := schnorr.SerializePubKey(server) + + if len(arkScript) > 0 { + arkScriptHash := ArkadeScriptHash(arkScript) + xonlySigner = schnorr.SerializePubKey(ComputeArkadeScriptPublicKey(server, arkScriptHash)) + } + for _, forfeit := range v.ForfeitClosures() { keys := make([]*btcec.PublicKey, 0) switch c := forfeit.(type) { @@ -121,13 +126,17 @@ func (v *TapscriptsVtxoScript) Validate( // must contain signer pubkey found := false for _, pubkey := range keys { - if bytes.Equal(schnorr.SerializePubKey(pubkey), xOnlySigner) { + if bytes.Equal(schnorr.SerializePubKey(pubkey), xonlySigner) { found = true break } } if !found { - return fmt.Errorf("invalid forfeit closure, signer pubkey not found") + pubkeysList := make([]string, 0) + for _, pubkey := range keys { + pubkeysList = append(pubkeysList, hex.EncodeToString(schnorr.SerializePubKey(pubkey))) + } + return fmt.Errorf("invalid forfeit closure, got signer pubkey not found: %s (expected: %s)", strings.Join(pubkeysList, ", "), hex.EncodeToString(xonlySigner)) } } diff --git a/pkg/ark-lib/txutils/psbt.go b/pkg/ark-lib/txutils/psbt.go index 45172975e..4e3900036 100644 --- a/pkg/ark-lib/txutils/psbt.go +++ b/pkg/ark-lib/txutils/psbt.go @@ -13,10 +13,12 @@ import ( ) var ( - COSIGNER_PSBT_KEY_PREFIX = []byte("cosigner") - CONDITION_WITNESS_KEY_PREFIX = []byte("condition") - VTXO_TREE_EXPIRY_PSBT_KEY = []byte("expiry") - VTXO_TAPROOT_TREE_KEY = []byte("taptree") + COSIGNER_PSBT_KEY_PREFIX = []byte("cosigner") + VTXO_TREE_EXPIRY_PSBT_KEY = []byte("expiry") + VTXO_TAPROOT_TREE_KEY = []byte("taptree") + ARKADE_SCRIPT_WITNESS_KEY_PREFIX = []byte("arkade_script_stack") + ARKADE_SCRIPT = []byte("arkade_script") + CONDITION_WITNESS_KEY_PREFIX = []byte("condition") ) // AddTaprootTree adds the whole taproot tree of the VTXO to the given PSBT input. @@ -181,3 +183,45 @@ func cosignerPrefixedKey(index int) []byte { func parsePrefixedCosignerKey(key []byte) bool { return bytes.HasPrefix(key, COSIGNER_PSBT_KEY_PREFIX) } + +func GetArkadeScript(in psbt.PInput) []byte { + for _, u := range in.Unknowns { + if bytes.Contains(u.Key, ARKADE_SCRIPT) { + return u.Value + } + } + + return nil +} + +func AddArkScript(inIndex int, ptx *psbt.Packet, script []byte) { + ptx.Inputs[inIndex].Unknowns = append(ptx.Inputs[inIndex].Unknowns, &psbt.Unknown{ + Value: script, + Key: ARKADE_SCRIPT, + }) +} + +func GetArkadeScriptWitness(in psbt.PInput) (wire.TxWitness, error) { + for _, u := range in.Unknowns { + if bytes.Contains(u.Key, ARKADE_SCRIPT_WITNESS_KEY_PREFIX) { + return ReadTxWitness(u.Value) + } + } + + return wire.TxWitness{}, nil +} + +func AddArkadeScriptWitness(inIndex int, ptx *psbt.Packet, witness wire.TxWitness) error { + var witnessBytes bytes.Buffer + + err := psbt.WriteTxWitness(&witnessBytes, witness) + if err != nil { + return err + } + + ptx.Inputs[inIndex].Unknowns = append(ptx.Inputs[inIndex].Unknowns, &psbt.Unknown{ + Value: witnessBytes.Bytes(), + Key: ARKADE_SCRIPT_WITNESS_KEY_PREFIX, + }) + return nil +} diff --git a/pkg/ark-lib/vtxo_script.go b/pkg/ark-lib/vtxo_script.go index 81ce3146a..04e967ae1 100644 --- a/pkg/ark-lib/vtxo_script.go +++ b/pkg/ark-lib/vtxo_script.go @@ -24,7 +24,7 @@ type TaprootTree interface { // // TODO: gather common and tree package to prevent circular dependency and move C generic type VtxoScript[T TaprootTree, C interface{}] interface { - Validate(signer *btcec.PublicKey, minLocktime RelativeLocktime, blockTypeAllowed bool) error + Validate(signer *btcec.PublicKey, minLocktime RelativeLocktime, blockTypeAllowed bool, arkadeScript []byte) error TapTree() (taprootKey *btcec.PublicKey, taprootScriptTree T, err error) Encode() ([]string, error) Decode(scripts []string) error diff --git a/pkg/arkd-wallet/core/psbt.go b/pkg/arkd-wallet/core/psbt.go index a253cf99e..eec069252 100644 --- a/pkg/arkd-wallet/core/psbt.go +++ b/pkg/arkd-wallet/core/psbt.go @@ -6,14 +6,20 @@ import ( "fmt" "time" + "github.com/arkade-os/arkd/pkg/ark-lib/script" + "github.com/arkade-os/arkd/pkg/ark-lib/txutils" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/waddrmgr" log "github.com/sirupsen/logrus" ) +// PsbtKeyTypeInputSignatureTweakSingle is a custom/proprietary PSBT key +// used by btcwallet to signal for custom private key tweak +var PsbtKeyTypeInputSignatureTweakSingle = []byte{0x51} var ANCHOR_PKSCRIPT = []byte{ 0x51, 0x02, 0x4e, 0x73, } @@ -115,8 +121,37 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e var managedAddress waddrmgr.ManagedPubKeyAddress isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript) + signed := false if len(in.TaprootLeafScript) > 0 { managedAddress = s.serverKeyAddr + + // if arkscript is present, the key must be tweaked before signing + // so we signal to btcwallet using the unknown PSBT field + arkscript := txutils.GetArkadeScript(*in) + if len(arkscript) > 0 { + privKey, err := managedAddress.PrivKey() + if err != nil { + return nil, err + } + tweakedPrivKey := script.ComputeArkadeScriptPrivateKey(privKey, script.ArkadeScriptHash(arkscript)) + preimage, err := getTaprootPreimage(packet, idx, in.TaprootLeafScript[0].Script) + if err != nil { + return nil, err + } + sig, err := schnorr.Sign(tweakedPrivKey, preimage) + if err != nil { + return nil, err + } + leafHash := txscript.NewBaseTapLeaf(in.TaprootLeafScript[0].Script).TapHash() + packet.Inputs[idx].TaprootScriptSpendSig = append(packet.Inputs[idx].TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{ + Signature: sig.Serialize(), + XOnlyPubKey: schnorr.SerializePubKey(tweakedPrivKey.PubKey()), + LeafHash: leafHash[:], + SigHash: txscript.SigHashDefault, + }) + signedInputs = append(signedInputs, uint32(idx)) + signed = true + } } else { var err error managedAddress, _, _, err = s.wallet.ScriptForOutput(in.WitnessUtxo) @@ -129,27 +164,27 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e } } - signedInputs = append(signedInputs, uint32(idx)) - - bip32Infos := derivationPathForAddress(managedAddress) - packet.Inputs[idx].Bip32Derivation = []*psbt.Bip32Derivation{bip32Infos} + if !signed { + bip32Infos := derivationPathForAddress(managedAddress) + packet.Inputs[idx].Bip32Derivation = []*psbt.Bip32Derivation{bip32Infos} - if isTaproot { - leafHashes := make([][]byte, 0, len(in.TaprootLeafScript)) - for _, leafScript := range in.TaprootLeafScript { - leafHash := txscript.NewBaseTapLeaf(leafScript.Script).TapHash() - leafHashes = append(leafHashes, leafHash[:]) - } + if isTaproot { + leafHashes := make([][]byte, 0, len(in.TaprootLeafScript)) + for _, leafScript := range in.TaprootLeafScript { + leafHash := txscript.NewBaseTapLeaf(leafScript.Script).TapHash() + leafHashes = append(leafHashes, leafHash[:]) + } - xonlypubkey := schnorr.SerializePubKey(managedAddress.PubKey()) + xonlypubkey := schnorr.SerializePubKey(managedAddress.PubKey()) - packet.Inputs[idx].TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{ - { - XOnlyPubKey: xonlypubkey, - MasterKeyFingerprint: bip32Infos.MasterKeyFingerprint, - Bip32Path: bip32Infos.Bip32Path, - LeafHashes: leafHashes, - }, + packet.Inputs[idx].TaprootBip32Derivation = []*psbt.TaprootBip32Derivation{ + { + XOnlyPubKey: xonlypubkey, + MasterKeyFingerprint: bip32Infos.MasterKeyFingerprint, + Bip32Path: bip32Infos.Bip32Path, + LeafHashes: leafHashes, + }, + } } } } @@ -159,13 +194,41 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e return nil, err } + signedInputs = append(signedInputs, ins...) + // delete derivation paths to avoid duplicate keys error - for idx := range signedInputs { + for idx := range ins { packet.Inputs[idx].Bip32Derivation = nil packet.Inputs[idx].TaprootBip32Derivation = nil } - return ins, nil + return signedInputs, nil +} + +func getTaprootPreimage( + tx *psbt.Packet, inputIndex int, leafScript []byte, +) ([]byte, error) { + prevouts := make(map[wire.OutPoint]*wire.TxOut) + + for i, input := range tx.Inputs { + if input.WitnessUtxo == nil { + return nil, fmt.Errorf("missing witness utxo on input #%d", i) + } + + outpoint := tx.UnsignedTx.TxIn[i].PreviousOutPoint + prevouts[outpoint] = input.WitnessUtxo + } + + prevoutFetcher := txscript.NewMultiPrevOutFetcher(prevouts) + + return txscript.CalcTapscriptSignaturehash( + txscript.NewTxSigHashes(tx.UnsignedTx, prevoutFetcher), + txscript.SigHashDefault, + tx.UnsignedTx, + inputIndex, + prevoutFetcher, + txscript.NewBaseTapLeaf(leafScript), + ) } func derivationPathForAddress(addr waddrmgr.ManagedPubKeyAddress) *psbt.Bip32Derivation { diff --git a/pkg/arkd-wallet/core/wallet_service.go b/pkg/arkd-wallet/core/wallet_service.go index 14788b030..4db8e20ac 100644 --- a/pkg/arkd-wallet/core/wallet_service.go +++ b/pkg/arkd-wallet/core/wallet_service.go @@ -485,19 +485,22 @@ func (s *service) SignTransaction(_ context.Context, partialTx string, extractRa if err != nil { return "", err } + args := make(map[string]any) - conditionWitness, err := txutils.GetConditionWitness(in) - if err != nil { - return "", err - } - - args := make(map[string][]byte) - if len(conditionWitness) > 0 { - var conditionWitnessBytes bytes.Buffer - if err := psbt.WriteTxWitness(&conditionWitnessBytes, conditionWitness); err != nil { + switch closure.(type) { + case *script.ConditionMultisigClosure: + witness, err := txutils.GetConditionWitness(in) + if err != nil { return "", err } - args[string(txutils.CONDITION_WITNESS_KEY_PREFIX)] = conditionWitnessBytes.Bytes() + + if len(witness) > 0 { + var conditionWitnessBytes bytes.Buffer + if err := psbt.WriteTxWitness(&conditionWitnessBytes, witness); err != nil { + return "", err + } + args[script.ConditionWitnessKey] = conditionWitnessBytes.Bytes() + } } for _, sig := range in.TaprootScriptSpendSig { diff --git a/pkg/arkd-wallet/go.mod b/pkg/arkd-wallet/go.mod index 8bbe42d7b..5d58b7cd0 100644 --- a/pkg/arkd-wallet/go.mod +++ b/pkg/arkd-wallet/go.mod @@ -9,6 +9,7 @@ replace github.com/arkade-os/arkd/pkg/ark-lib => ../ark-lib replace github.com/arkade-os/arkd/api-spec => ../../api-spec require ( + github.com/ark-network/ark/common v0.0.0-20250606113434-241d3e1ec7cb github.com/arkade-os/arkd/api-spec v0.0.0-00010101000000-000000000000 github.com/arkade-os/arkd/pkg/ark-lib v0.0.0-00010101000000-000000000000 github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53 @@ -196,7 +197,7 @@ require ( lukechampine.com/blake3 v1.3.0 // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect modernc.org/libc v1.59.3 // indirect - modernc.org/mathutil v1.6.0 // indirect + modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.33.1 // indirect modernc.org/strutil v1.2.0 // indirect diff --git a/pkg/arkd-wallet/go.sum b/pkg/arkd-wallet/go.sum index ee8adbb83..a3f6805c9 100644 --- a/pkg/arkd-wallet/go.sum +++ b/pkg/arkd-wallet/go.sum @@ -18,6 +18,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSi github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/ark-network/ark/common v0.0.0-20250606113434-241d3e1ec7cb h1:TvNEA7OJF3CZU5A7wdXOHBTgrIDU4KZ+IBxi6+WEC1g= +github.com/ark-network/ark/common v0.0.0-20250606113434-241d3e1ec7cb/go.mod h1:A8c6gJaMt6wTDkZCPY8UpQmFkHBpBwg+zb1RD/wbRq4= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -762,8 +764,8 @@ modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbP modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/libc v1.59.3 h1:A4QAp1lRSn2/b4aU+wBtq+yeKgq/2BUevrj0p1ZNy2M= modernc.org/libc v1.59.3/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 65f265a4b..7b531ea70 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -17,7 +17,9 @@ import ( "testing" "time" + "github.com/ark-network/ark/common" arklib "github.com/arkade-os/arkd/pkg/ark-lib" + "github.com/arkade-os/arkd/pkg/ark-lib/arkade" "github.com/arkade-os/arkd/pkg/ark-lib/offchain" "github.com/arkade-os/arkd/pkg/ark-lib/script" "github.com/arkade-os/arkd/pkg/ark-lib/txutils" @@ -1739,6 +1741,270 @@ func TestSendToConditionMultisigClosure(t *testing.T) { require.NoError(t, err) } +// TestSendToArkadeScriptClosure tests sending funds to an arkade script closure +// alice onboard funds and send to an arkade script closure using introspection opcodes +// then 2 offchain transactions are attempted: +// 1. offchain with the wrong outputs : test if it fails +// 2. offchain with the correct outputs : test if it succeeds +func TestSendToArkadeScriptClosure(t *testing.T) { + ctx := context.Background() + alice, grpcAlice := setupArkSDK(t) + defer grpcAlice.Close() + + bobPrivKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + configStore, err := inmemorystoreconfig.NewConfigStore() + require.NoError(t, err) + + walletStore, err := inmemorystore.NewWalletStore() + require.NoError(t, err) + + bobWallet, err := singlekeywallet.NewBitcoinWallet( + configStore, + walletStore, + ) + require.NoError(t, err) + + _, err = bobWallet.Create(ctx, password, hex.EncodeToString(bobPrivKey.Serialize())) + require.NoError(t, err) + + _, err = bobWallet.Unlock(ctx, password) + require.NoError(t, err) + + bobPubKey := bobPrivKey.PubKey() + + // Fund Alice's account + _, offchainAddr, boardingAddress, err := alice.Receive(ctx) + require.NoError(t, err) + + aliceAddr, err := arklib.DecodeAddressV0(offchainAddr) + require.NoError(t, err) + + _, err = runCommand("nigiri", "faucet", boardingAddress) + require.NoError(t, err) + + time.Sleep(5 * time.Second) + + _, err = alice.Settle(ctx) + require.NoError(t, err) + + time.Sleep(5 * time.Second) + + const sendAmount = 10000 + + alicePkScript, err := common.P2TRScript(aliceAddr.VtxoTapKey) + require.NoError(t, err) + + // script verifying that the spending tx includes an output going to alice's address + arkadeScript, err := txscript.NewScriptBuilder(). + AddInt64(0). + AddOp(arkade.OP_INSPECTOUTPUTSCRIPTPUBKEY). + AddOp(arkade.OP_1). + AddOp(arkade.OP_EQUALVERIFY). + AddData(alicePkScript[2:]). // only witness program is pushed + AddOp(arkade.OP_EQUAL). + Script() + require.NoError(t, err) + + vtxoScript := script.TapscriptsVtxoScript{ + Closures: []script.Closure{ + &script.MultisigClosure{ + PubKeys: []*btcec.PublicKey{ + bobPubKey, + script.ComputeArkadeScriptPublicKey( + aliceAddr.Signer, + script.ArkadeScriptHash(arkadeScript), + ), + }, + }, + }, + } + + vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree() + require.NoError(t, err) + + closure := vtxoScript.ForfeitClosures()[0] + + bobAddr := arklib.Address{ + HRP: "tark", + VtxoTapKey: vtxoTapKey, + Signer: aliceAddr.Signer, + } + + arkadeTapscript, err := closure.Script() + require.NoError(t, err) + + merkleProof, err := vtxoTapTree.GetTaprootMerkleProof( + txscript.NewBaseTapLeaf(arkadeTapscript).TapHash(), + ) + require.NoError(t, err) + + ctrlBlock, err := txscript.ParseControlBlock(merkleProof.ControlBlock) + require.NoError(t, err) + + tapscript := &waddrmgr.Tapscript{ + ControlBlock: ctrlBlock, + RevealedScript: merkleProof.Script, + } + + bobAddrStr, err := bobAddr.EncodeV0() + require.NoError(t, err) + + txid, err := alice.SendOffChain( + ctx, + false, + []types.Receiver{{To: bobAddrStr, Amount: sendAmount}}, + ) + require.NoError(t, err) + require.NotEmpty(t, txid) + + indexerSvc := setupIndexer(t) + + fundingTx, err := indexerSvc.GetVirtualTxs(ctx, []string{txid}) + require.NoError(t, err) + require.NotEmpty(t, fundingTx) + require.Len(t, fundingTx.Txs, 1) + + redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(fundingTx.Txs[0]), true) + require.NoError(t, err) + + var bobOutput *wire.TxOut + var bobOutputIndex uint32 + for i, out := range redeemPtx.UnsignedTx.TxOut { + if bytes.Equal(out.PkScript[2:], schnorr.SerializePubKey(bobAddr.VtxoTapKey)) { + bobOutput = out + bobOutputIndex = uint32(i) + break + } + } + require.NotNil(t, bobOutput) + + infos, err := grpcAlice.GetInfo(ctx) + require.NoError(t, err) + unilateralExitDelayType := arklib.LocktimeTypeSecond + if infos.UnilateralExitDelay < 512 { + unilateralExitDelayType = arklib.LocktimeTypeBlock + } + + signerUnrollClosure := &script.CSVMultisigClosure{ + Locktime: arklib.RelativeLocktime{ + Type: unilateralExitDelayType, + Value: uint32(infos.UnilateralExitDelay), + }, + MultisigClosure: script.MultisigClosure{ + PubKeys: []*btcec.PublicKey{aliceAddr.Signer}, + }, + } + + validTx, validCheckpoints, err := offchain.BuildTxs( + []offchain.VtxoInput{ + { + Outpoint: &wire.OutPoint{ + Hash: redeemPtx.UnsignedTx.TxHash(), + Index: bobOutputIndex, + }, + Tapscript: tapscript, + Amount: bobOutput.Value, + RevealedTapscripts: []string{hex.EncodeToString(arkadeTapscript)}, + ArkadeScript: arkadeScript, + }, + }, + []*wire.TxOut{ + { + Value: bobOutput.Value, + PkScript: alicePkScript, + }, + }, + signerUnrollClosure, + ) + require.NoError(t, err) + + arkadeScriptFromPsbt := txutils.GetArkadeScript(validTx.Inputs[0]) + require.Equal(t, hex.EncodeToString(arkadeScript), hex.EncodeToString(arkadeScriptFromPsbt)) + + invalidTx, invalidCheckpoints, err := offchain.BuildTxs( + []offchain.VtxoInput{ + { + Outpoint: &wire.OutPoint{ + Hash: redeemPtx.UnsignedTx.TxHash(), + Index: bobOutputIndex, + }, + Tapscript: tapscript, + Amount: bobOutput.Value, + RevealedTapscripts: []string{hex.EncodeToString(arkadeTapscript)}, + ArkadeScript: arkadeScript, + }, + }, + []*wire.TxOut{ + { + Value: bobOutput.Value, + PkScript: []byte{0x6a}, // output 0 is not alice script + }, + }, + signerUnrollClosure, + ) + require.NoError(t, err) + + encodedInvalidTx, err := invalidTx.B64Encode() + require.NoError(t, err) + + explorer := explorer.NewExplorer("http://localhost:3000", arklib.BitcoinRegTest) + + signedInvalidTx, err := bobWallet.SignTransaction( + ctx, + explorer, + encodedInvalidTx, + ) + require.NoError(t, err) + + encodedInvalidCheckpoints := make([]string, 0, len(invalidCheckpoints)) + for _, checkpoint := range invalidCheckpoints { + encoded, err := checkpoint.B64Encode() + require.NoError(t, err) + encodedInvalidCheckpoints = append(encodedInvalidCheckpoints, encoded) + } + + _, _, _, err = grpcAlice.SubmitTx(ctx, signedInvalidTx, encodedInvalidCheckpoints) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid ark tx signature(s): arkade script execution failed") + + encodedValidTx, err := validTx.B64Encode() + require.NoError(t, err) + + signedTx, err := bobWallet.SignTransaction( + ctx, + explorer, + encodedValidTx, + ) + require.NoError(t, err) + + encodedValidCheckpoints := make([]string, 0, len(validCheckpoints)) + for _, checkpoint := range validCheckpoints { + encoded, err := checkpoint.B64Encode() + require.NoError(t, err) + encodedValidCheckpoints = append(encodedValidCheckpoints, encoded) + } + + txid, _, signedCheckpoints, err := grpcAlice.SubmitTx(ctx, signedTx, encodedValidCheckpoints) + require.NoError(t, err) + + finalCheckpoints := make([]string, 0, len(signedCheckpoints)) + for _, checkpoint := range signedCheckpoints { + finalCheckpoint, err := bobWallet.SignTransaction( + ctx, + explorer, + checkpoint, + ) + require.NoError(t, err) + + finalCheckpoints = append(finalCheckpoints, finalCheckpoint) + } + + err = grpcAlice.FinalizeTx(ctx, txid, finalCheckpoints) + require.NoError(t, err) +} + func TestDeleteIntent(t *testing.T) { ctx := context.Background() alice, grpcAlice := setupArkSDK(t)