diff --git a/.golangci.yml b/.golangci.yml index f706d88..056983b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -268,6 +268,12 @@ linters-settings: require-explanation: true # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. require-specific: true + revive: + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments + - name: package-comments + severity: warning + disabled: true rowserrcheck: packages: - github.com/jmoiron/sqlx @@ -353,6 +359,10 @@ linters: - goerr113 - testpackage - nlreturn + - deadcode + - scopelint + - varcheck + - structcheck - gci disable-all: false presets: diff --git a/Makefile b/Makefile index e8a3172..856a48b 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ run-linter: install-linter: - @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.35.2 + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.49.0 go-doc-mac: @open http://localhost:6060 && \ diff --git a/merchant.go b/beneficiary.go similarity index 58% rename from merchant.go rename to beneficiary.go index 26deb2c..00e22a6 100644 --- a/merchant.go +++ b/beneficiary.go @@ -1,7 +1,16 @@ +/* +Package dpp defines some kind of DPP structure. + +These structures are defined in the TSC spec: +https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +This comment is here to qualify as the required make-work for the jobsworth who implemented the +revive "package-comment" linter. +*/ package dpp -// Merchant to be displayed to the user. -type Merchant struct { +// Beneficiary to be displayed to the user. +type Beneficiary struct { // AvatarURL displays a canonical url to a merchants avatar. AvatarURL string `json:"avatar" example:"http://url.com"` // Name is a human readable string identifying the merchant. @@ -12,4 +21,6 @@ type Merchant struct { Address string `json:"address" example:"1 the street, the town, B1 1AA"` // ExtendedData can be supplied if the merchant wishes to send some arbitrary data back to the wallet. ExtendedData map[string]interface{} `json:"extendedData,omitempty"` + // PaymentReference ID of invoice. + PaymentReference string `json:"paymentReference" example:"Order-325214"` } diff --git a/destinations.go b/destinations.go deleted file mode 100644 index 7e937d4..0000000 --- a/destinations.go +++ /dev/null @@ -1,35 +0,0 @@ -package dpp - -import ( - "time" - - "github.com/libsv/go-bt/v2" - "github.com/libsv/go-bt/v2/bscript" -) - -// Output message used in BIP270. -// See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#output -type Output struct { - // Amount is the number of satoshis to be paid. - Amount uint64 `json:"amount" example:"100000"` - // Script is a locking script where payment should be sent, formatted as a hexadecimal string. - LockingScript *bscript.Script `json:"script" swaggertype:"primitive,string" example:"76a91455b61be43392125d127f1780fb038437cd67ef9c88ac"` - // Description, an optional description such as "tip" or "sales tax". Maximum length is 100 chars. - Description string `json:"description" example:"paymentReference 123456"` -} - -// PaymentDestinations contains the supported destinations -// by this DPP server. -type PaymentDestinations struct { - Outputs []Output `json:"outputs"` -} - -// Destinations message containing outputs and their fees. -type Destinations struct { - AncestryRequired bool `json:"ancestryRequired"` - Network string `json:"network"` - Outputs []Output `json:"outputs"` - Fees *bt.FeeQuote `json:"fees"` - CreatedAt time.Time `json:"createdAt"` - ExpiresAt time.Time `json:"expiresAt"` -} diff --git a/mocks/mocks.go b/mocks/mocks.go index 6278b29..5fa99db 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -2,4 +2,4 @@ package mocks //go:generate moq -pkg mocks -out payment_writer.go ../ PaymentWriter //go:generate moq -pkg mocks -out payment_service.go ../ PaymentService -//go:generate moq -pkg mocks -out payment_request_service.go ../ PaymentRequestService +//go:generate moq -pkg mocks -out payment_request_service.go ../ PaymentTermsService diff --git a/mocks/payment_request_service.go b/mocks/payment_request_service.go index a3b249a..1a28548 100644 --- a/mocks/payment_request_service.go +++ b/mocks/payment_request_service.go @@ -5,77 +5,79 @@ package mocks import ( "context" + "github.com/libsv/go-bk/envelope" "github.com/libsv/go-dpp" "sync" ) -// Ensure, that PaymentRequestServiceMock does implement dpp.PaymentRequestService. +// Ensure, that PaymentTermsServiceMock does implement dpp.PaymentTermsService. // If this is not the case, regenerate this file with moq. -var _ dpp.PaymentRequestService = &PaymentRequestServiceMock{} +var _ dpp.PaymentTermsService = &PaymentTermsServiceMock{} -// PaymentRequestServiceMock is a mock implementation of dpp.PaymentRequestService. +// PaymentTermsServiceMock is a mock implementation of dpp.PaymentTermsService. // -// func TestSomethingThatUsesPaymentRequestService(t *testing.T) { +// func TestSomethingThatUsesPaymentTermsService(t *testing.T) { // -// // make and configure a mocked dpp.PaymentRequestService -// mockedPaymentRequestService := &PaymentRequestServiceMock{ -// PaymentRequestFunc: func(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentRequest, error) { -// panic("mock out the PaymentRequest method") -// }, -// } +// // make and configure a mocked dpp.PaymentTermsService +// mockedPaymentTermsService := &PaymentTermsServiceMock{ +// PaymentTermsFunc: func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { +// panic("mock out the PaymentTerms method") +// }, +// } // -// // use mockedPaymentRequestService in code that requires dpp.PaymentRequestService -// // and then make assertions. +// // use mockedPaymentTermsService in code that requires dpp.PaymentTermsService +// // and then make assertions. // -// } -type PaymentRequestServiceMock struct { - // PaymentRequestFunc mocks the PaymentRequest method. - PaymentRequestFunc func(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentRequest, error) +// } +type PaymentTermsServiceMock struct { + // PaymentTermsFunc mocks the PaymentTerms method. + PaymentTermsFunc func(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) // calls tracks calls to the methods. calls struct { - // PaymentRequest holds details about calls to the PaymentRequest method. - PaymentRequest []struct { + // PaymentTerms holds details about calls to the PaymentTerms method. + PaymentTerms []struct { // Ctx is the ctx argument value. Ctx context.Context // Args is the args argument value. - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs } } - lockPaymentRequest sync.RWMutex + lockPaymentTerms sync.RWMutex } -// PaymentRequest calls PaymentRequestFunc. -func (mock *PaymentRequestServiceMock) PaymentRequest(ctx context.Context, args dpp.PaymentRequestArgs) (*dpp.PaymentRequest, error) { - if mock.PaymentRequestFunc == nil { - panic("PaymentRequestServiceMock.PaymentRequestFunc: method is nil but PaymentRequestService.PaymentRequest was just called") +// PaymentTerms calls PaymentTermsFunc. +func (mock *PaymentTermsServiceMock) PaymentTerms(ctx context.Context, args dpp.PaymentTermsArgs) (*envelope.JSONEnvelope, error) { + if mock.PaymentTermsFunc == nil { + panic("PaymentTermsServiceMock.PaymentTermsFunc: method is nil but PaymentTermsService.PaymentTerms was just called") } callInfo := struct { Ctx context.Context - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs }{ Ctx: ctx, Args: args, } - mock.lockPaymentRequest.Lock() - mock.calls.PaymentRequest = append(mock.calls.PaymentRequest, callInfo) - mock.lockPaymentRequest.Unlock() - return mock.PaymentRequestFunc(ctx, args) + mock.lockPaymentTerms.Lock() + mock.calls.PaymentTerms = append(mock.calls.PaymentTerms, callInfo) + mock.lockPaymentTerms.Unlock() + return mock.PaymentTermsFunc(ctx, args) } -// PaymentRequestCalls gets all the calls that were made to PaymentRequest. +// PaymentTermsCalls gets all the calls that were made to PaymentTerms. // Check the length with: -// len(mockedPaymentRequestService.PaymentRequestCalls()) -func (mock *PaymentRequestServiceMock) PaymentRequestCalls() []struct { +// +// len(mockedPaymentTermsService.PaymentTermsCalls()) +func (mock *PaymentTermsServiceMock) PaymentTermsCalls() []struct { Ctx context.Context - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs } { var calls []struct { Ctx context.Context - Args dpp.PaymentRequestArgs + Args dpp.PaymentTermsArgs } - mock.lockPaymentRequest.RLock() - calls = mock.calls.PaymentRequest - mock.lockPaymentRequest.RUnlock() + mock.lockPaymentTerms.RLock() + calls = mock.calls.PaymentTerms + mock.lockPaymentTerms.RUnlock() return calls } diff --git a/mocks/payment_service.go b/mocks/payment_service.go index 4d8c433..1259b22 100644 --- a/mocks/payment_service.go +++ b/mocks/payment_service.go @@ -15,19 +15,19 @@ var _ dpp.PaymentService = &PaymentServiceMock{} // PaymentServiceMock is a mock implementation of dpp.PaymentService. // -// func TestSomethingThatUsesPaymentService(t *testing.T) { +// func TestSomethingThatUsesPaymentService(t *testing.T) { // -// // make and configure a mocked dpp.PaymentService -// mockedPaymentService := &PaymentServiceMock{ -// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { -// panic("mock out the PaymentCreate method") -// }, -// } +// // make and configure a mocked dpp.PaymentService +// mockedPaymentService := &PaymentServiceMock{ +// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { +// panic("mock out the PaymentCreate method") +// }, +// } // -// // use mockedPaymentService in code that requires dpp.PaymentService -// // and then make assertions. +// // use mockedPaymentService in code that requires dpp.PaymentService +// // and then make assertions. // -// } +// } type PaymentServiceMock struct { // PaymentCreateFunc mocks the PaymentCreate method. PaymentCreateFunc func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) @@ -69,7 +69,8 @@ func (mock *PaymentServiceMock) PaymentCreate(ctx context.Context, args dpp.Paym // PaymentCreateCalls gets all the calls that were made to PaymentCreate. // Check the length with: -// len(mockedPaymentService.PaymentCreateCalls()) +// +// len(mockedPaymentService.PaymentCreateCalls()) func (mock *PaymentServiceMock) PaymentCreateCalls() []struct { Ctx context.Context Args dpp.PaymentCreateArgs diff --git a/mocks/payment_writer.go b/mocks/payment_writer.go index fcdd485..8e4e196 100644 --- a/mocks/payment_writer.go +++ b/mocks/payment_writer.go @@ -15,19 +15,19 @@ var _ dpp.PaymentWriter = &PaymentWriterMock{} // PaymentWriterMock is a mock implementation of dpp.PaymentWriter. // -// func TestSomethingThatUsesPaymentWriter(t *testing.T) { +// func TestSomethingThatUsesPaymentWriter(t *testing.T) { // -// // make and configure a mocked dpp.PaymentWriter -// mockedPaymentWriter := &PaymentWriterMock{ -// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { -// panic("mock out the PaymentCreate method") -// }, -// } +// // make and configure a mocked dpp.PaymentWriter +// mockedPaymentWriter := &PaymentWriterMock{ +// PaymentCreateFunc: func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) { +// panic("mock out the PaymentCreate method") +// }, +// } // -// // use mockedPaymentWriter in code that requires dpp.PaymentWriter -// // and then make assertions. +// // use mockedPaymentWriter in code that requires dpp.PaymentWriter +// // and then make assertions. // -// } +// } type PaymentWriterMock struct { // PaymentCreateFunc mocks the PaymentCreate method. PaymentCreateFunc func(ctx context.Context, args dpp.PaymentCreateArgs, req dpp.Payment) (*dpp.PaymentACK, error) @@ -69,7 +69,8 @@ func (mock *PaymentWriterMock) PaymentCreate(ctx context.Context, args dpp.Payme // PaymentCreateCalls gets all the calls that were made to PaymentCreate. // Check the length with: -// len(mockedPaymentWriter.PaymentCreateCalls()) +// +// len(mockedPaymentWriter.PaymentCreateCalls()) func (mock *PaymentWriterMock) PaymentCreateCalls() []struct { Ctx context.Context Args dpp.PaymentCreateArgs diff --git a/modes/hybridmode/hybrid_payment.go b/modes/hybridmode/hybrid_payment.go new file mode 100644 index 0000000..2b6edcf --- /dev/null +++ b/modes/hybridmode/hybrid_payment.go @@ -0,0 +1,26 @@ +/* +Package hybridmode defines the subset of the DPP payment terms structure related to the hybrid mode. + +These structures are defined in the TSC spec: +https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +This comment is here to qualify as the required make-work for the jobsworth who implemented the +revive "package-comment" linter. +*/ +package hybridmode + +import "github.com/libsv/go-bc/spv" + +// Payment includes data required for hybridmode payment mode. +type Payment struct { + // OptionID ID of chosen payment options + OptionID string `json:"optionId"` + // Transactions A list of valid, signed Bitcoin transactions that fully pays the PaymentTerms. + // The transaction is hex-encoded and must NOT be prefixed with “0x”. + // The order of transactions should match the order from PaymentTerms for this mode. + Transactions []string `json:"transactions"` + // Ancestors a map of txid to ancestry transaction info for the transactions in above + // each ancestor contains the TX together with the MerkleProof needed when SPVRequired is true. + // See: https://tsc.bitcoinassociation.net/standards/transaction-ancestors/ + Ancestors map[string]spv.TSCAncestryJSON `json:"ancestors"` +} diff --git a/modes/hybridmode/hybrid_payment_ack.go b/modes/hybridmode/hybrid_payment_ack.go new file mode 100644 index 0000000..f41a858 --- /dev/null +++ b/modes/hybridmode/hybrid_payment_ack.go @@ -0,0 +1,17 @@ +package hybridmode + + +// PeerChannelData holds peer channel information for subscribing to and reading from a peer channel. +type PeerChannelData struct { + Host string `json:"host"` + Path string `json:"path"` + ChannelID string `json:"channel_id"` + Token string `json:"token"` +} + + +// PaymentACK includes data required for hybridmode payment mode. +type PaymentACK struct { + TransactionIds []string `json:"transactionIds"` + PeerChannel *PeerChannelData `json:"peerChannel"` +} diff --git a/modes/hybridmode/hybrid_payment_terms.go b/modes/hybridmode/hybrid_payment_terms.go new file mode 100644 index 0000000..791c0bc --- /dev/null +++ b/modes/hybridmode/hybrid_payment_terms.go @@ -0,0 +1,38 @@ +package hybridmode + +import ( + "github.com/libsv/go-dpp/nativetypes" +) + +// Policies An object containing some policy information like fees or whether Ancestors are +// required in the `Payment`. +type Policies struct { + // FeeRate defines the amount of fees a users wallet should add to the payment + // when submitting their final payments. + FeeRate map[string]map[string]int `json:"fees,omitempty"` + SPVRequired bool `json:"SPVRequired,omitempty"` + LockTime uint32 `json:"lockTime,omitempty"` +} + +// Inputs provides options of different arrays of input script types. +// Currently, only "native" type input are supported. +type Inputs struct { + NativeOutputs []nativetypes.NativeInput `json:"native"` +} + +// Outputs provides options of different arrays of output script types. +// Currently, only "native" type outputs are supported. +type Outputs struct { + NativeOutputs []nativetypes.NativeOutput `json:"native"` +} + +// TransactionTerms a single definition of requested transaction format for the standard payment mode: +// "ef63d9775da5" in the DPP TSC spec. +type TransactionTerms struct { + Outputs Outputs `json:"outputs"` + Inputs Inputs `json:"inputs,omitempty"` + Policies *Policies `json:"policies"` +} + +// PaymentTerms message used in DPP TSC spec. for the `PaymentTerms` message. +type PaymentTerms map[string]map[string][]TransactionTerms diff --git a/nativetypes/native_types.go b/nativetypes/native_types.go new file mode 100644 index 0000000..4fb7bb3 --- /dev/null +++ b/nativetypes/native_types.go @@ -0,0 +1,34 @@ +/* +Package nativetypes defines some kind of DPP structure. + +These structures are defined in the TSC spec: +https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +This comment is here to qualify as the required make-work for the jobsworth who implemented the +revive "package-comment" linter. +*/ +package nativetypes + +import ( + "github.com/libsv/go-bt/v2/bscript" +) + +// NativeOutput defines a native type output as opposed to a token for example. +type NativeOutput struct { + // Amount is the number of satoshis to be paid. + Amount uint64 `json:"amount" binding:"required" example:"100000"` + // Script is a locking script where payment should be sent, formatted as a hexadecimal string. + LockingScript *bscript.Script `json:"script" binding:"required" swaggertype:"primitive,string" example:"76a91455b61be43392125d127f1780fb038437cd67ef9c88ac"` + // Description, an optional description such as "tip" or "sales tax". Maximum length is 100 chars. + Description string `json:"description,omitempty" example:"paymentReference 123456"` +} + +// NativeInput a way of declaring requirements for the inputs which should be used. It is "native" to distinguish it +// from a token input in the hybridmode payment mode. +type NativeInput struct { + ScriptSig string `json:"scriptSig" binding:"required"` // string. required. + TxID string `json:"txid" binding:"required"` // string. required. + Vout uint32 `json:"vout" binding:"required"` // integer. required. + Value uint64 `json:"value" binding:"required"` // integer. required. + NSequence int `json:"nSequence,omitempty"` // number. optional. +} diff --git a/payment.go b/payment.go index 32c0a41..47db75d 100644 --- a/payment.go +++ b/payment.go @@ -2,65 +2,48 @@ package dpp import ( "context" - - "github.com/libsv/go-bt/v2" - "github.com/pkg/errors" + "github.com/libsv/go-dpp/modes/hybridmode" validator "github.com/theflyingcodr/govalidator" ) +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +// Originator Data about payer. This data might be needed in many cases, e.g. tracking data for later loyalty +// points processing etc. +type Originator struct { + // Name name of payer. + Name string `json:"name"` + // Paymail Payer’s paymail (where e.g. refunds will be send, identity can be use somehow etc.). + Paymail string `json:"paymail"` + // Avatar URL to an avatar. + Avatar string `json:"avatar"` + // ExtendedData additional optional data. + ExtendedData map[string]interface{} `json:"extendedData"` +} + // Payment is a Payment message used in BIP270. // See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#payment type Payment struct { - // MerchantData is copied from PaymentDetails.merchantData. - // Payment hosts may use invoice numbers or any other data they require to match Payments to PaymentRequests. - // Note that malicious clients may modify the merchantData, so should be authenticated - // in some way (for example, signed with a payment host-only key). - // Maximum length is 10000 characters. - MerchantData Merchant `json:"merchantData"` - // RefundTo is a paymail to send a refund to should a refund be necessary. - // Maximum length is 100 characters - RefundTo *string `json:"refundTo" swaggertype:"primitive,string" example:"me@paymail.com"` - // Memo is a plain-text note from the customer to the payment host. - Memo string `json:"memo" example:"for invoice 123456"` - // Ancestry which contains the details of previous transaction and Merkle proof of each input UTXO. - // Should be available if AncestryRequired is set to true in the paymentRequest. - // See https://tsc.bitcoinassociation.net/standards/spv-envelope/ - Ancestry *string `json:"ancestry"` - // RawTX should be sent if AncestryRequired is set to false in the payment request. - RawTx *string `json:"rawTx"` - // ProofCallbacks are optional and can be supplied when the sender wants to receive - // a merkleproof for the transaction they are submitting as part of the SPV Envelope. - // - // This is especially useful if they are receiving change and means when they use it - // as an input, they can provide the merkle proof. - ProofCallbacks map[string]ProofCallback `json:"proofCallbacks"` + // ModeID chosen from possible messages of PaymentTerms. + ModeID string `json:"modeId" binding:"required" example:"ef63d9775da5"` + // Mode Object with data required by specific mode, e.g. HybridPaymentTerms + Mode hybridmode.Payment `json:"mode" binding:"required"` + // Originator Data about payer. This data might be needed in many cases, e.g. refund, tract data for later loyalty points processing etc. + Originator Originator `json:"originator"` + // Transaction A single valid, signed Bitcoin transaction that fully pays the PaymentTerms. This field is deprecated. + Transaction *string `json:"transaction,omitempty"` + // Memo A plain-text note from the customer to the payment host. + Memo string `json:"memo,omitempty"` } // Validate will ensure the users request is correct. func (p Payment) Validate() error { v := validator.New(). - Validate("ancestry/rawTx", func() error { - if p.RawTx == nil { - return errors.New("either ancestry or a rawTX are required") - } - return nil - }). - Validate("merchantData.extendedData", validator.NotEmpty(p.MerchantData.ExtendedData)) - if p.MerchantData.ExtendedData != nil { - v = v.Validate("merchantData.paymentReference", validator.NotEmpty(p.MerchantData.ExtendedData["paymentReference"])) - } - - if p.RawTx != nil { - v = v.Validate("rawTx", func() error { - if _, err := bt.NewTxFromString(*p.RawTx); err != nil { - return errors.Wrap(err, "invalid rawTx supplied") - } - return nil - }) - } - if p.RefundTo != nil { - v = v.Validate("refundTo", validator.StrLength(*p.RefundTo, 0, 100)) - } + Validate("modeId", validator.NotEmpty(p.ModeID)). + Validate("mode", validator.NotEmpty(p.Mode)). + Validate("mode.optionId", validator.NotEmpty(p.Mode.OptionID)). + Validate("mode.transactions", validator.NotEmpty(p.Mode.Transactions)) return v.Err() } @@ -70,28 +53,6 @@ type ProofCallback struct { Token string `json:"token"` } -// PaymentACK message used in BIP270. -// See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#paymentack -type PaymentACK struct { - ID string `json:"id"` - TxID string `json:"tx_id"` - Memo string `json:"memo"` - PeerChannel *PeerChannelData `json:"peer_channel"` - // A number indicating why the transaction was not accepted. 0 or undefined indicates no error. - // A 1 or any other positive integer indicates an error. The errors are left undefined for now; - // it is recommended only to use “1” and to fill the memo with a textual explanation about why - // the transaction was not accepted until further numbers are defined and standardised. - Error int `json:"error,omitempty"` -} - -// PeerChannelData holds peer channel information for subscribing to and reading from a peer channel. -type PeerChannelData struct { - Host string `json:"host"` - Path string `json:"path"` - ChannelID string `json:"channel_id"` - Token string `json:"token"` -} - // PaymentCreateArgs identifies the paymentID used for the payment. type PaymentCreateArgs struct { PaymentID string `param:"paymentID"` diff --git a/payment_ack.go b/payment_ack.go new file mode 100644 index 0000000..ed7d733 --- /dev/null +++ b/payment_ack.go @@ -0,0 +1,19 @@ +package dpp + +import ( + "github.com/libsv/go-dpp/modes/hybridmode" +) + +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +// PaymentACK message used in the TSC DPP spec. +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol/#PaymentModes +type PaymentACK struct { + // ModeID the chosen mode. + ModeID string `json:"modeId" binding:"required" example:"ef63d9775da5"` + // Mode data required by specific payment mode + Mode *hybridmode.PaymentACK `json:"mode"` + PeerChannel *hybridmode.PeerChannelData `json:"peerChannel"` + RedirectURL string `json:"redirectUrl"` +} diff --git a/payment_request.go b/payment_request.go deleted file mode 100644 index d016a86..0000000 --- a/payment_request.go +++ /dev/null @@ -1,61 +0,0 @@ -package dpp - -import ( - "context" - "time" - - "github.com/libsv/go-bt/v2" -) - -// PaymentRequest message used in BIP270. -// See https://github.com/moneybutton/bips/blob/master/bip-0270.mediawiki#paymentrequest -type PaymentRequest struct { - // Network Always set to "bitcoin" (but seems to be set to 'bitcoin-sv' - // outside bip270 spec, see https://handcash.github.io/handcash-merchant-integration/#/merchant-payments) - // {enum: bitcoin, bitcoin-sv, test} - // Required. - Network string `json:"network" example:"mainnet" enums:"mainnet,testnet,stn,regtest"` - // AncestryRequired if true will expect the sender to submit an ancestry in the payment request, otherwise - // a rawTx will be required. - AncestryRequired bool `json:"ancestryRequired" example:"true"` - // Destinations contains supported payment destinations by the merchant and dpp server, initial P2PKH outputs but can be extended. - // Required. - Destinations PaymentDestinations `json:"destinations"` - // CreationTimestamp Unix timestamp (seconds since 1-Jan-1970 UTC) when the PaymentRequest was created. - // Required. - CreationTimestamp time.Time `json:"creationTimestamp" swaggertype:"primitive,string" example:"2019-10-12T07:20:50.52Z"` - // ExpirationTimestamp Unix timestamp (UTC) after which the PaymentRequest should be considered invalid. - // Optional. - ExpirationTimestamp time.Time `json:"expirationTimestamp" swaggertype:"primitive,string" example:"2019-10-12T07:20:50.52Z"` - // PaymentURL secure HTTPS location where a Payment message (see below) will be sent to obtain a PaymentACK. - // Maximum length is 4000 characters - PaymentURL string `json:"paymentUrl" example:"https://localhost:3443/api/v1/payment/123456"` - // Memo Optional note that should be displayed to the customer, explaining what this PaymentRequest is for. - // Maximum length is 50 characters. - Memo string `json:"memo" example:"invoice number 123456"` - // MerchantData contains arbitrary data that may be used by the payment host to identify the PaymentRequest. - // May be omitted if the payment host does not need to associate Payments with PaymentRequest - // or if they associate each PaymentRequest with a separate payment address. - // Maximum length is 10000 characters. - MerchantData *Merchant `json:"merchantData,omitempty"` - // FeeRate defines the amount of fees a users wallet should add to the payment - // when submitting their final payments. - FeeRate *bt.FeeQuote `json:"fees"` -} - -// PaymentRequestArgs are request arguments that can be passed to the service. -type PaymentRequestArgs struct { - // PaymentID is an identifier for an invoice. - PaymentID string `param:"paymentID"` -} - -// PaymentRequestService can be implemented to enforce business rules -// and process in order to fulfil a PaymentRequest. -type PaymentRequestService interface { - PaymentRequestReader -} - -// PaymentRequestReader will return a new payment request. -type PaymentRequestReader interface { - PaymentRequest(ctx context.Context, args PaymentRequestArgs) (*PaymentRequest, error) -} diff --git a/payment_terms.go b/payment_terms.go new file mode 100644 index 0000000..79b2c07 --- /dev/null +++ b/payment_terms.go @@ -0,0 +1,73 @@ +package dpp + +import ( + "context" + + "github.com/libsv/go-bk/envelope" + "github.com/libsv/go-dpp/modes/hybridmode" + "github.com/libsv/go-dpp/nativetypes" +) + +// These structures are defined in the TSC spec: +// See https://tsc.bitcoinassociation.net/standards/direct_payment_protocol + +// PaymentTermsModes message used in DPP TSC spec. for the `PaymentTerms` message. +type PaymentTermsModes struct { + // Hybrid contains a key value map of possible payment terms modalities - currently there is only one option: + // `HybridPaymentMode` with BRFCID: "ef63d9775da5". + Hybrid hybridmode.PaymentTerms `json:"ef63d9775da5"` +} + +// PaymentTerms message as defined in the DPP T$C spec. +type PaymentTerms struct { + // Network Always set to "bitcoin" (but seems to be set to 'bitcoin-sv' + // outside bip270 spec, see https://handcash.github.io/handcash-merchant-integration/#/merchant-payments) + // {enum: bitcoin, bitcoin-sv, test} + // Required. + Network string `json:"network" binding:"required" example:"mainnet" enums:"mainnet,testnet,stn,regtest"` + // Version version of DPP TSC spec. + // Required. + Version string `json:"version" binding:"required" example:"1.0"` + // Outputs an array of outputs. DEPRECATED but included for backward compatibility. + // Optional. + Outputs []nativetypes.NativeOutput `json:"outputs,omitempty"` + // CreationTimestamp Unix timestamp (seconds since 1-Jan-1970 UTC) when the PaymentTerms were created. + // Required. + CreationTimestamp int64 `json:"creationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648163657"` + // ExpirationTimestamp Unix timestamp (UTC) after which the PaymentTerms should be considered invalid. + // Optional. + ExpirationTimestamp int64 `json:"expirationTimestamp" binding:"required" swaggertype:"primitive,int" example:"1648164657"` + // PaymentURL secure HTTPS location where a Payment message (see below) will be sent to obtain a PaymentACK. + // Maximum length is 4000 characters + PaymentURL string `json:"paymentUrl" binding:"required" example:"http://localhost:3443/api/v1/payment/123456"` + // Memo note that should be displayed to the customer, explaining what these PaymentTerms are for. + // Maximum length is 50 characters. + // Optional. + Memo string `json:"memo,omitempty" example:"invoice number 123456"` + // Beneficiary Arbitrary data that may be used by the payment host to identify the PaymentTerms + // May be omitted if the payment host does not need to associate Payments with PaymentTerms + // or if they associate each PaymentTerms with a separate payment address. + // Maximum length is 10000 characters. + // Optional. + Beneficiary *Beneficiary `json:"beneficiary,omitempty"` + // PaymentTermsModes TSC payment messages specified by ID (and well defined) messages customer can choose to pay + // A key-value map. required field but not if legacy BIP270 outputs are provided + Modes *PaymentTermsModes `json:"modes"` +} + +// PaymentTermsArgs are request arguments that can be passed to the service. +type PaymentTermsArgs struct { + // PaymentID is an identifier for an invoice. + PaymentID string `param:"paymentID"` +} + +// PaymentTermsService can be implemented to enforce business rules +// and process in order to fulfil PaymentTerms. +type PaymentTermsService interface { + PaymentTermsReader +} + +// PaymentTermsReader will return a new payment request. +type PaymentTermsReader interface { + PaymentTerms(ctx context.Context, args PaymentTermsArgs) (*envelope.JSONEnvelope, error) +} diff --git a/payment_test.go b/payment_test.go index 5ffd815..1659a4a 100644 --- a/payment_test.go +++ b/payment_test.go @@ -1,6 +1,7 @@ package dpp import ( + "github.com/libsv/go-dpp/modes/hybridmode" "testing" "github.com/matryer/is" @@ -14,64 +15,57 @@ func TestPaymentCreate_Validate(t *testing.T) { }{ "valid request should return no errors": { req: Payment{ - MerchantData: Merchant{ + ModeID: "ef63d9775da5", + Mode: hybridmode.Payment{ + OptionID: "choiceID0", + Transactions: []string{"tx1", "tx2"}, + }, + Originator: Originator{ + Name: "Bob The Builder", ExtendedData: map[string]interface{}{ "paymentReference": "abc123", }, }, - ProofCallbacks: map[string]ProofCallback{ - "http://myproofs/proofs": { - Token: "abc123", - }, - }, - RawTx: func() *string { - s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" - return &s - }(), - Memo: "test this please", - }, - }, "merchant data missing extended data should error": { - req: Payment{ - RawTx: func() *string { + Transaction: func() *string { s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" return &s }(), - Memo: "test this please", }, - exp: "[merchantData.extendedData: value cannot be empty]", - }, "merchant data missing payment reference should error": { + }, "mode id missing should error": { req: Payment{ - RawTx: func() *string { - s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" - return &s - }(), - MerchantData: Merchant{ExtendedData: map[string]interface{}{"test": "value"}}, - Memo: "test this please", + Mode: hybridmode.Payment{ + OptionID: "choiceID0", + Transactions: []string{ + "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000", + }, + }, + Originator: Originator{ + Name: "Bob The Builder", + ExtendedData: map[string]interface{}{ + "paymentReference": "abc123", + }, + }, }, - exp: "[merchantData.paymentReference: value cannot be empty]", - }, "refundTo too long should error": { + exp: "[modeId: value cannot be empty]", + }, "mode data missing payment should error": { req: Payment{ - RawTx: func() *string { - s := "0200000004c4b8372f640f9fab1dc2c14eda6a9669d13ca0f4fff42c318f388cf917399fa9000000004847304402203f2c94003474010010a11cdc4bfac3065e117b22ff1e218fb31230be12a80d5202205b69e27a1815a7d6668a5b73e57b15a6117c94b15b3d915ff3304803e233af5341feffffff417e443a9da68f5bea767bb90f09737df50ff7592d662407dc16ed17af0b821d000000006a47304402200fe1bb41b168aa1e071b39c1bd00d7f960d98406b36c76cbeff98acbe20c117902205628cf5755676f85b2cd360406fc771ed3244395d2cd2bf2292e06e0a8f7e4dc412103b811b71802653c97388faa8a7275a49a2742896285515fb01e2801948ee9cc4cfeffffff94b976366984846918b8ef346da50db6231dcf870c6d48754a98976b3a989c23000000004847304402201baa75b71f066eaa5297efaa878f215fd08e3132e3de2d5c7038e8433ef49cf8022044655ef242869210ed8a9a290c5ccc7cfa70a0d6b8cc7d6dc832d1d728ef106341feffffff4383ff843f365a8c9a6ce44ba1c584840125227e7ad06409f7194423ca614aff000000006a4730440220328b446736fa1a47e8675e7ea31a86f6025ece36aa2e158e21e85758a1cf1db8022073cf6f9f3353337a537bfbfef818497941b6f00f6918d40e87d06751610e739e412102065bd35d20f59e1c8c1254690254f14e40710409481320df3854bbfc867b4698feffffff027a898400000000001976a914fc54fbfac51db40cd845ebe6d243d6c950f4bf4088ac0065cd1d000000001976a914ba903fcaa03a280a9577da32db79e52373b8d0e388ac1b040000" - return &s - }(), - RefundTo: func() *string { - bb := make([]byte, 0) - // generate string 1 more byte than 10000 - for i := 0; i <= 100; i++ { - bb = append(bb, 42) - } - out := string(bb) - return &out - }(), - MerchantData: Merchant{ + ModeID: "ef63d9775da5", + Originator: Originator{ + Name: "Bob The Builder", ExtendedData: map[string]interface{}{ "paymentReference": "abc123", }, }, - Memo: "test this please", }, - exp: "[refundTo: value must be between 0 and 100 characters]", + exp: "[mode.optionId: value cannot be empty], [mode.transactions: value cannot be empty], [mode: value cannot be empty]", + }, "mode missing transaction field should error": { + req: Payment{ + ModeID: "ef63d9775da5", + Mode: hybridmode.Payment{ + OptionID: "choiceID0", + }, + }, + exp: "[mode.transactions: value cannot be empty]", }, } for name, test := range tests { diff --git a/vendor/github.com/libsv/go-bc/spv/ancestry_binary.go b/vendor/github.com/libsv/go-bc/spv/ancestry_binary.go new file mode 100644 index 0000000..73f460f --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/ancestry_binary.go @@ -0,0 +1,142 @@ +package spv + +import ( + "github.com/libsv/go-bk/crypto" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript" + + "github.com/libsv/go-bc" +) + +const ( + flagTx = byte(1) + flagProof = byte(2) + flagMapi = byte(3) +) + +// Payment is a payment and its ancestry. +type Payment struct { + PaymentTx *bt.Tx + Ancestry []byte +} + +// binaryChunk is a clear way to pass around chunks while keeping their type explicit. +type binaryChunk struct { + ContentType byte + Data []byte +} + +type extendedInput struct { + input *bt.Input + vin int +} + +type ancestry struct { + Tx *bt.Tx + Proof []byte + MapiResponses []*bc.MapiCallback +} + +// parseAncestry creates a new struct from the bytes of a txContext. +func parseAncestry(b []byte) (map[[32]byte]*ancestry, error) { + + if b[0] != 1 { // the first byte is the version number. + return nil, ErrUnsupporredVersion + } + offset := uint64(1) + total := uint64(len(b)) + aa := make(map[[32]byte]*ancestry) + + var TxID [32]byte + + if total == offset { + return nil, ErrCannotCalculateFeePaid + } + + // first Data must be a Tx + if b[offset] != 1 { + return nil, ErrTipTxConfirmed + } + + for total > offset { + chunk, size := parseChunk(b, offset) + offset += size + switch chunk.ContentType { + case flagTx: + hash := crypto.Sha256d(chunk.Data) + copy(TxID[:], bt.ReverseBytes(hash)) // fixed size array from slice. + tx, err := bt.NewTxFromBytes(chunk.Data) + if err != nil { + return nil, err + } + if len(tx.Inputs) == 0 { + return nil, ErrNoTxInputsToVerify + } + aa[TxID] = &ancestry{ + Tx: tx, + } + case flagProof: + aa[TxID].Proof = chunk.Data + case flagMapi: + callBacks, err := parseMapiCallbacks(chunk.Data) + if err != nil { + return nil, err + } + aa[TxID].MapiResponses = callBacks + default: + continue + } + } + return aa, nil +} + +func parseChunk(b []byte, start uint64) (binaryChunk, uint64) { + offset := start + typeOfNextData := b[offset] + offset++ + l, size := bt.NewVarIntFromBytes(b[offset:]) + offset += uint64(size) + chunk := binaryChunk{ + ContentType: typeOfNextData, + Data: b[offset : offset+uint64(l)], + } + offset += uint64(l) + return chunk, offset - start +} + +func parseMapiCallbacks(b []byte) ([]*bc.MapiCallback, error) { + if len(b) == 0 { + return nil, ErrTriedToParseZeroBytes + } + var internalOffset uint64 + allBinary := uint64(len(b)) + numOfMapiResponses := b[internalOffset] + if numOfMapiResponses == 0 && len(b) == 1 { + return nil, ErrTriedToParseZeroBytes + } + internalOffset++ + + var responses = [][]byte{} + for allBinary > internalOffset { + l, size := bt.NewVarIntFromBytes(b[internalOffset:]) + internalOffset += uint64(size) + response := b[internalOffset : internalOffset+uint64(l)] + internalOffset += uint64(l) + responses = append(responses, response) + } + + mapiResponses := make([]*bc.MapiCallback, 0) + for _, response := range responses { + mapiResponse, err := bc.NewMapiCallbackFromBytes(response) + if err != nil { + return nil, err + } + mapiResponses = append(mapiResponses, mapiResponse) + } + return mapiResponses, nil +} + +func verifyInputOutputPair(tx *bt.Tx, lock *bscript.Script, unlock *bscript.Script) bool { + // TODO script interpreter. + return true +} diff --git a/vendor/github.com/libsv/go-bc/spv/ancestry_json.go b/vendor/github.com/libsv/go-bc/spv/ancestry_json.go new file mode 100644 index 0000000..272b446 --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/ancestry_json.go @@ -0,0 +1,100 @@ +package spv + +import ( + "encoding/hex" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" + + "github.com/libsv/go-bc" +) + +// AncestryJSON is a struct which contains all information needed for a transaction to be verified. +// this contains all ancestors for the transaction allowing proofs etc to be verified. +// +// NOTE: this is the JSON format of the Ancestry but in a nested format (in comparison) with +// the flat structure that the TSC uses. This allows verification to become a lot easier and +// use a recursive function. +type AncestryJSON struct { + TxID string `json:"txid,omitempty"` + RawTx string `json:"rawTx,omitempty"` + Proof *bc.MerkleProof `json:"proof,omitempty"` + MapiResponses []bc.MapiCallback `json:"mapiResponses,omitempty"` + Parents map[string]*AncestryJSON `json:"parents,omitempty"` +} + +// IsAnchored returns true if the ancestry has a merkle proof. +func (e *AncestryJSON) IsAnchored() bool { + return e.Proof != nil +} + +// HasParents returns true if this ancestry has immediate parents. +func (e *AncestryJSON) HasParents() bool { + return e.Parents != nil && len(e.Parents) > 0 +} + +// ParentTx will return a parent if found and convert the rawTx to a bt.TX, otherwise a ErrNotAllInputsSupplied error is returned. +func (e *AncestryJSON) ParentTx(txID string) (*bt.Tx, error) { + env, ok := e.Parents[txID] + if !ok { + return nil, errors.Wrapf(ErrNotAllInputsSupplied, "expected parent tx %s is missing", txID) + } + return bt.NewTxFromString(env.RawTx) +} + +// Bytes takes a TxAncestry struct and returns the serialised binary format. +func (e *AncestryJSON) Bytes() ([]byte, error) { + ancestryBinary := make([]byte, 0) + ancestryBinary = append(ancestryBinary, 1) // Binary format version 1 + binary, err := serialiseInputs(e.Parents) + if err != nil { + return nil, err + } + ancestryBinary = append(ancestryBinary, binary...) + return ancestryBinary, nil +} + +func serialiseInputs(parents map[string]*AncestryJSON) ([]byte, error) { + binary := make([]byte, 0) + for _, input := range parents { + currentTx, err := hex.DecodeString(input.RawTx) + if err != nil { + return nil, err + } + dataLength := bt.VarInt(uint64(len(currentTx))) + binary = append(binary, flagTx) // first data will always be a rawTx. + binary = append(binary, dataLength.Bytes()...) // of this length. + binary = append(binary, currentTx...) // the data. + if input.MapiResponses != nil && len(input.MapiResponses) > 0 { + binary = append(binary, flagMapi) // next data will be a mapi response. + numMapis := bt.VarInt(uint64(len(input.MapiResponses))) + binary = append(binary, numMapis.Bytes()...) // number of mapi reponses which follow + for _, mapiResponse := range input.MapiResponses { + mapiR, err := mapiResponse.Bytes() + if err != nil { + return nil, err + } + dataLength := bt.VarInt(uint64(len(mapiR))) + binary = append(binary, dataLength.Bytes()...) // of this length. + binary = append(binary, mapiR...) // the data. + } + } + if input.Proof != nil { + proof, err := input.Proof.Bytes() + if err != nil { + return nil, errors.Wrap(err, "Failed to serialise this input's proof struct") + } + proofLength := bt.VarInt(uint64(len(proof))) + binary = append(binary, flagProof) // it's going to be a proof. + binary = append(binary, proofLength.Bytes()...) // of this length. + binary = append(binary, proof...) // the data. + } else if input.HasParents() { + parentsBinary, err := serialiseInputs(input.Parents) + if err != nil { + return nil, err + } + binary = append(binary, parentsBinary...) + } + } + return binary, nil +} diff --git a/vendor/github.com/libsv/go-bc/spv/ancestry_tsc_json.go b/vendor/github.com/libsv/go-bc/spv/ancestry_tsc_json.go new file mode 100644 index 0000000..e897923 --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/ancestry_tsc_json.go @@ -0,0 +1,112 @@ +package spv + +import ( + "encoding/hex" + + "github.com/libsv/go-bc" + "github.com/libsv/go-bt/v2" +) + +// TSCAncestriesJSON spec at https://tsc.bitcoinassociation.net/standards/transaction-ancestors/ eventually. +type TSCAncestriesJSON []TSCAncestryJSON + +// TSCAncestryJSON is one of the serial objects within the overall list of ancestors. +// +// NOTE: This JSON structure follows the TSC definition even though the other JSON +// structure used in ancestry_json.go is more useful for verification. +type TSCAncestryJSON struct { + RawTx string `json:"rawtx,omitempty"` + Proof *bc.MerkleProof `json:"proof,omitempty"` + MapiResponses []*bc.MapiCallback `json:"mapiResponses,omitempty"` +} + +// NewAncestryJSONFromBytes is a way to create the JSON format for Ancestry from the binary format. +func NewAncestryJSONFromBytes(b []byte) (TSCAncestriesJSON, error) { + ancestry, err := parseAncestry(b) + if err != nil { + return nil, err + } + ancestors := make([]TSCAncestryJSON, 0) + for _, ancestor := range ancestry { + rawTx := ancestor.Tx.String() + a := TSCAncestryJSON{ + RawTx: rawTx, + MapiResponses: ancestor.MapiResponses, + } + if ancestor.Proof != nil { + mpb, err := parseBinaryMerkleProof(ancestor.Proof) + if err != nil { + return nil, err + } + a.Proof = &bc.MerkleProof{ + Index: mpb.index, + TxOrID: mpb.txOrID, + Target: mpb.target, + Nodes: mpb.nodes, + ProofType: flagProofType(mpb.flags), + } + } + ancestors = append(ancestors, a) + } + return ancestors, nil +} + +// Bytes takes an AncestryJSON and returns the serialised bytes. +func (j TSCAncestriesJSON) Bytes() ([]byte, error) { + binaryTxContext := make([]byte, 0) + + // Binary format version 1. + binaryTxContext = append(binaryTxContext, 1) + + // follow with the list of ancestors, including their proof or mapi responses if present. + for _, ancestor := range j { + rawTx, err := hex.DecodeString(ancestor.RawTx) + if err != nil { + return nil, err + } + length := bt.VarInt(uint64(len(rawTx))) + binaryTxContext = append(binaryTxContext, flagTx) + binaryTxContext = append(binaryTxContext, length.Bytes()...) + binaryTxContext = append(binaryTxContext, rawTx...) + if ancestor.Proof != nil { + rawProof, err := ancestor.Proof.Bytes() + if err != nil { + return nil, err + } + length := bt.VarInt(uint64(len(rawProof))) + binaryTxContext = append(binaryTxContext, flagProof) + binaryTxContext = append(binaryTxContext, length.Bytes()...) + binaryTxContext = append(binaryTxContext, rawProof...) + } + if ancestor.MapiResponses != nil && len(ancestor.MapiResponses) > 0 { + binaryTxContext = append(binaryTxContext, flagMapi) + numOfMapiResponses := bt.VarInt(uint64(len(ancestor.MapiResponses))) + binaryTxContext = append(binaryTxContext, numOfMapiResponses.Bytes()...) + for _, mapiResponse := range ancestor.MapiResponses { + mapiR, err := mapiResponse.Bytes() + if err != nil { + return nil, err + } + dataLength := bt.VarInt(uint64(len(mapiR))) + binaryTxContext = append(binaryTxContext, dataLength.Bytes()...) + binaryTxContext = append(binaryTxContext, mapiR...) + } + } + } + + return binaryTxContext, nil +} + +func flagProofType(flags byte) string { + switch flags & targetTypeFlags { + // if bits 1 and 2 of flags are NOT set, target should contain a block hash (32 bytes). + // if bit 2 of flags is set, target should contain a merkle root (32 bytes). + case 0, 4: + return "blockhash" + // if bit 1 of flags is set, target should contain a block header (80 bytes). + case 2: + return "header" + default: + return "" + } +} diff --git a/vendor/github.com/libsv/go-bc/spv/create_ancestry.go b/vendor/github.com/libsv/go-bc/spv/create_ancestry.go new file mode 100644 index 0000000..35ede7b --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/create_ancestry.go @@ -0,0 +1,66 @@ +package spv + +import ( + "context" + "fmt" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" +) + +// CreateTxAncestry builds and returns an spv.TxAncestry for the provided tx. +func (c *creator) CreateTxAncestry(ctx context.Context, tx *bt.Tx) (*AncestryJSON, error) { + if len(tx.Inputs) == 0 { + return nil, ErrNoTxInputs + } + + ancestry := &AncestryJSON{ + TxID: tx.TxID(), + RawTx: tx.String(), + Parents: make(map[string]*AncestryJSON), + } + + for _, input := range tx.Inputs { + pTxID := input.PreviousTxIDStr() + + // If we already have added the tx to the parent ancestry, there's no point in + // redoing the same work + if _, ok := ancestry.Parents[pTxID]; ok { + continue + } + + // Build a *bt.Tx from its TxID and recursively call this function building + // for inputs without proofs, until a parent with a Merkle Proof is found. + pTx, err := c.txc.Tx(ctx, pTxID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get tx %s", pTxID) + } + if pTx == nil { + return nil, fmt.Errorf("could not find tx %s", pTxID) + } + + // Check the store for a Merkle Proof for the current input. + mp, err := c.mpc.MerkleProof(ctx, pTxID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get merkle proof for tx %s", pTxID) + } + // If a Merkle Proof is found, create the ancestry and skip any further recursion + if mp != nil { + ancestry.Parents[pTxID] = &AncestryJSON{ + RawTx: pTx.String(), + TxID: pTxID, + Proof: mp, + } + continue + } + + pEnvelope, err := c.CreateTxAncestry(ctx, pTx) + if err != nil { + return nil, err + } + + ancestry.Parents[pTxID] = pEnvelope + } + + return ancestry, nil +} diff --git a/vendor/github.com/libsv/go-bc/spv/creator.go b/vendor/github.com/libsv/go-bc/spv/creator.go new file mode 100644 index 0000000..3fc12ae --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/creator.go @@ -0,0 +1,47 @@ +package spv + +import ( + "context" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" + + "github.com/libsv/go-bc" +) + +// An TxAncestryCreator is an interface used to build the spv.TxAncestry data type for +// Simple Payment Verification (SPV). +// +// The implementation of an spv.TxStore and spv.MerkleProofStore which is supplied will depend +// on the client you are using. +type TxAncestryCreator interface { + CreateTxAncestry(context.Context, *bt.Tx) (*AncestryJSON, error) +} + +// TxStore interfaces the a tx store. +type TxStore interface { + Tx(ctx context.Context, txID string) (*bt.Tx, error) +} + +// MerkleProofStore interfaces a Merkle Proof store. +type MerkleProofStore interface { + MerkleProof(ctx context.Context, txID string) (*bc.MerkleProof, error) +} + +type creator struct { + txc TxStore + mpc MerkleProofStore +} + +// NewEnvelopeCreator creates a new spv.Creator with the provided spv.TxStore and tx.MerkleProofStore. +// If either implementation is not provided, the setup will return an error. +func NewEnvelopeCreator(txc TxStore, mpc MerkleProofStore) (TxAncestryCreator, error) { + if txc == nil { + return nil, errors.New("an spv.TxStore implementation is required") + } + if mpc == nil { + return nil, errors.New("an spv.MerkleProofStore implementation is required") + } + + return &creator{txc: txc, mpc: mpc}, nil +} diff --git a/vendor/github.com/libsv/go-bc/spv/errors.go b/vendor/github.com/libsv/go-bc/spv/errors.go new file mode 100644 index 0000000..482fda3 --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/errors.go @@ -0,0 +1,70 @@ +package spv + +import "github.com/pkg/errors" + +var ( + // ErrNoTxInputs returns if an ancestry is attempted to be created from a transaction that has no inputs. + ErrNoTxInputs = errors.New("provided tx has no inputs to build ancestry from") + + // ErrPaymentNotVerified returns if a transaction in the tree provided was missed during verification. + ErrPaymentNotVerified = errors.New("a tx was missed during validation") + + // ErrTipTxConfirmed returns if the tip transaction is already confirmed. + ErrTipTxConfirmed = errors.New("tip transaction must be unconfirmed") + + // ErrNoConfirmedTransaction returns if a path from tip to beginning/anchor contains no confirmed transaction. + ErrNoConfirmedTransaction = errors.New("not confirmed/anchored tx(s) provided") + + // ErrTxIDMismatch returns if they key value pair of a transactions input has a mismatch in txID. + ErrTxIDMismatch = errors.New("input and proof ID mismatch") + + // ErrNotAllInputsSupplied returns if an unconfirmed transaction in ancestry contains inputs which are not + // present in the parent ancestor. + ErrNotAllInputsSupplied = errors.New("a tx input missing in parent ancestor") + + // ErrNoTxInputsToVerify returns if a transaction has no inputs. + ErrNoTxInputsToVerify = errors.New("a tx has no inputs to verify") + + // ErrNilInitialPayment returns if a transaction has no inputs. + ErrNilInitialPayment = errors.New("initial payment cannot be nil") + + // ErrInputRefsOutOfBoundsOutput returns if a transaction has no inputs. + ErrInputRefsOutOfBoundsOutput = errors.New("tx input index into output is out of bounds") + + // ErrNoFeeQuoteSupplied is returned when VerifyFees is enabled but no bt.FeeQuote has been supplied. + ErrNoFeeQuoteSupplied = errors.New("no bt.FeeQuote supplied for fee validation, supply the bt.FeeQuote using VerifyFees opt") + + // ErrFeePaidNotEnough returned when not enough fees have been paid. + ErrFeePaidNotEnough = errors.New("not enough fees paid") + + // ErrCannotCalculateFeePaid returned when fee check is enabled but the tx has no parents. + ErrCannotCalculateFeePaid = errors.New("no parents supplied in ancestry which means we cannot valdiate " + + "fees, either ensure parents are supplied or remove fee check") + + // ErrInvalidProof is returned if the merkle proof validation fails. + ErrInvalidProof = errors.New("invalid merkle proof, payment invalid") + + // ErrMissingOutput is returned when checking fees if an output in a parent tx is missing. + ErrMissingOutput = errors.New("expected output used in payment tx missing") + + // ErrProofOrInputMissing returns if a path from tip to beginning/anchor is broken. + ErrProofOrInputMissing = errors.New("break in the ancestry missing either a parent transaction or a proof") + + // ErrTriedToParseZeroBytes returns when we attempt to parse a slice of bytes of zero length which should be a mapi response. + ErrTriedToParseZeroBytes = errors.New("there are no mapi response bytes to parse") + + // ErrUnsupporredVersion returns if another version of the binary format is being used - since we cannot guarantee we know how to parse it. + ErrUnsupporredVersion = errors.New("we only support version 1 of the Ancestor Binary format") + + // ErrInvalidMerkleFlags returns if a merkle proof being verified uses something other than the one currently supported. + ErrInvalidMerkleFlags = errors.New("invalid flags used in merkle proof") + + // ErrMissingTxidInProof returns if there's a missing txid in the proof. + ErrMissingTxidInProof = errors.New("missing txid in proof") + + // ErrMissingRootInProof returns if there's a missing root in the proof. + ErrMissingRootInProof = errors.New("missing root in proof") + + // ErrInvalidNodes returns if there is a * on the left hand side within the node array. + ErrInvalidNodes = errors.New("invalid nodes") +) diff --git a/vendor/github.com/libsv/go-bc/spv/verifier.go b/vendor/github.com/libsv/go-bc/spv/verifier.go new file mode 100644 index 0000000..d3066ca --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/verifier.go @@ -0,0 +1,156 @@ +package spv + +import ( + "context" + + "github.com/libsv/go-bt/v2" + "github.com/pkg/errors" + + "github.com/libsv/go-bc" +) + +type verifyOptions struct { + // proofs validation + proofs bool + script bool + fees bool + feeQuote *bt.FeeQuote +} + +// clone will copy the verifyOptions to a new struct and return it. +func (v *verifyOptions) clone() *verifyOptions { + return &verifyOptions{ + proofs: v.proofs, + fees: v.fees, + script: v.script, + feeQuote: v.feeQuote, + } +} + +// VerifyOpt defines a functional option that is used to modify behaviour of +// the payment verifier. +type VerifyOpt func(opts *verifyOptions) + +// VerifyProofs will make the verifier validate the ancestry merkle proofs for each parent transaction. +func VerifyProofs() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = true + } +} + +// NoVerifyProofs will switch off ancestry proof verification +// and rely on mAPI/node verification when the tx is broadcast. +func NoVerifyProofs() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = false + } +} + +// VerifyFees will make the verifier check the transaction fees +// of the supplied transaction are enough based on the feeQuote +// provided. +// +// It is recommended to provide a fresh fee quote when calling the VerifyPayment +// method rather than loading fees when calling NewPaymentVerifier as fees can go out of date +// over the lifetime of the application and you may be supplying different feeQuotes +// to different consumers. +func VerifyFees(fees *bt.FeeQuote) VerifyOpt { + return func(opts *verifyOptions) { + opts.fees = true + opts.feeQuote = fees + } +} + +// NoVerifyFees will switch off transaction fee verification and rely on +// mAPI / node verification when the transaction is broadcast. +func NoVerifyFees() VerifyOpt { + return func(opts *verifyOptions) { + opts.fees = false + opts.feeQuote = nil + } +} + +// VerifyScript will ensure the scripts provided in the transaction are valid. +func VerifyScript() VerifyOpt { + return func(opts *verifyOptions) { + opts.script = true + } +} + +// NoVerifyScript will switch off script verification and rely on +// mAPI / node verification when the tx is broadcast. +func NoVerifyScript() VerifyOpt { + return func(opts *verifyOptions) { + opts.script = false + } +} + +// NoVerifySPV will turn off any spv validation for merkle proofs +// and script validation. This is a helper method that is equivalent to +// NoVerifyProofs && NoVerifyScripts. +func NoVerifySPV() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = false + opts.script = false + } +} + +// VerifySPV will turn on spv validation for merkle proofs +// and script validation. This is a helper method that is equivalent to +// VerifyProofs && VerifyScripts. +func VerifySPV() VerifyOpt { + return func(opts *verifyOptions) { + opts.proofs = true + opts.script = true + } +} + +// A PaymentVerifier is an interface used to complete Simple Payment Verification (SPV) +// in conjunction with a Merkle Proof. +// +// The implementation of bc.BlockHeaderChain which is supplied will depend on the client +// you are using, some may return a HeaderJSON response others may return the blockhash. +type PaymentVerifier interface { + VerifyPayment(ctx context.Context, p *Payment, opts ...VerifyOpt) error + MerkleProofVerifier +} + +// MerkleProofVerifier interfaces the verification of Merkle Proofs. +type MerkleProofVerifier interface { + VerifyMerkleProof(context.Context, []byte) (*MerkleProofValidation, error) + VerifyMerkleProofJSON(context.Context, *bc.MerkleProof) (bool, bool, error) +} + +type verifier struct { + // BlockHeaderChain will be set when an implementation returning a bc.BlockHeader type is provided. + bhc bc.BlockHeaderChain + opts *verifyOptions +} + +// NewPaymentVerifier creates a new spv.PaymentVerifer with the bc.BlockHeaderChain provided. +// If no BlockHeaderChain implementation is provided, the setup will return an error. +// +// opts control the global behaviour of the verifier and all options are enabled by default, they are: +// - ancestry verification (proofs checked etc) +// - fees checked, ensuring the root tx covers enough fees +// - script verification which checks the script is correct (not currently implemented). +func NewPaymentVerifier(bhc bc.BlockHeaderChain, opts ...VerifyOpt) (PaymentVerifier, error) { + o := &verifyOptions{ + proofs: true, + fees: false, + script: true, + } + for _, opt := range opts { + opt(o) + } + if o.proofs && bhc == nil { + return nil, errors.New("at least one blockchain header implementation should be returned") + } + return &verifier{bhc: bhc, opts: o}, nil +} + +// NewMerkleProofVerifier creates a new spv.MerkleProofVerifer with the bc.BlockHeaderChain provided. +// If no BlockHeaderChain implementation is provided, the setup will return an error. +func NewMerkleProofVerifier(bhc bc.BlockHeaderChain) (MerkleProofVerifier, error) { + return NewPaymentVerifier(bhc) +} diff --git a/vendor/github.com/libsv/go-bc/spv/verifymerkleproof.go b/vendor/github.com/libsv/go-bc/spv/verifymerkleproof.go new file mode 100644 index 0000000..e4f00bc --- /dev/null +++ b/vendor/github.com/libsv/go-bc/spv/verifymerkleproof.go @@ -0,0 +1,328 @@ +package spv + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + + "github.com/libsv/go-bt/v2" + + "github.com/libsv/go-bc" +) + +const ( + txOrIDFlag byte = 1 << iota // 1 << 0 which is 00000001 + targetTypeFlag1 // 1 << 1 which is 00000010 + targetTypeFlag2 // 1 << 2 which is 00000100 + proofTypeFlag // 1 << 3 which is 00001000 + compositeFlag // 1 << 4 which is 00010000 + + targetTypeFlags = targetTypeFlag1 | targetTypeFlag2 +) + +// MerkleProofValidation is a wrapper for the response of a validation operation. +type MerkleProofValidation struct { + TxID string + Valid bool + IsLastInTree bool +} + +// VerifyMerkleProof verifies a Merkle Proof in standard byte format. +func (v *verifier) VerifyMerkleProof(ctx context.Context, proof []byte) (*MerkleProofValidation, error) { + mpb, err := parseBinaryMerkleProof(proof) + if err != nil { + return nil, err + } + + err = validateTxOrID(mpb.flags, mpb.txOrID) + if err != nil { + return nil, err + } + + txid, err := txidFromTxOrID(mpb.txOrID) + if err != nil { + return nil, err + } + + response := &MerkleProofValidation{ + TxID: txid, + } + + var merkleRoot string + switch mpb.flags & targetTypeFlags { + // if bits 1 and 2 of flags are NOT set, target should contain a block hash (32 bytes) + case 0: + // The `target` field contains a block hash + blockHeader, err := v.bhc.BlockHeader(ctx, mpb.target) + if err != nil { + return response, err + } + + merkleRoot = blockHeader.HashMerkleRootStr() + + // if bit 2 of flags is set, target should contain a merkle root (32 bytes) + case 4: + // the `target` field contains a merkle root + merkleRoot = mpb.target + + // if bit 1 of flags is set, target should contain a block header (80 bytes) + case 2: + // The `target` field contains a block header + var err error + merkleRoot, err = bc.ExtractMerkleRootFromBlockHeader(mpb.target) + if err != nil { + return response, err + } + + default: + return response, ErrInvalidMerkleFlags + } + + if mpb.flags&proofTypeFlag == 1 { + return response, ErrInvalidMerkleFlags + } + + if mpb.flags&compositeFlag == 1 { + return response, ErrInvalidMerkleFlags // composite proof type not supported + } + + if txid == "" { + return response, ErrMissingTxidInProof + } + + if merkleRoot == "" { + return response, ErrMissingRootInProof + } + + valid, isLastInTree, err := verifyProof(txid, merkleRoot, mpb.index, mpb.nodes) + + return &MerkleProofValidation{ + TxID: txid, + Valid: valid, + IsLastInTree: isLastInTree, + }, err +} + +// VerifyMerkleProofJSON verifies a Merkle Proof in standard JSON format. +func (v *verifier) VerifyMerkleProofJSON(ctx context.Context, proof *bc.MerkleProof) (bool, bool, error) { + + txid, err := txidFromTxOrID(proof.TxOrID) + if err != nil { + return false, false, err + } + + var merkleRoot string + if proof.TargetType == "" || proof.TargetType == "hash" { + // The `target` field contains a block hash + + if len(proof.Target) != 64 { + return false, false, errors.New("invalid target field") + } + + blockHeader, err := v.bhc.BlockHeader(ctx, proof.Target) + if err != nil { + return false, false, err + } + merkleRoot = blockHeader.HashMerkleRootStr() + + } else if proof.TargetType == "header" && len(proof.Target) == 160 { + // The `target` field contains a block header + var err error + merkleRoot, err = bc.ExtractMerkleRootFromBlockHeader(proof.Target) + if err != nil { + return false, false, err + } + + } else if proof.TargetType == "merkleRoot" && len(proof.Target) == 64 { + // the `target` field contains a merkle root + merkleRoot = proof.Target + + } else { + return false, false, errors.New("invalid TargetType or target field") + } + + if proof.ProofType != "" && proof.ProofType != "branch" { + return false, false, errors.New("only merkle branch supported in this version") // merkle tree proof type not supported + } + + if proof.Composite { // OR if (proof.composite && proof.composite != false) + return false, false, errors.New("only single proof supported in this version") // composite proof type not supported + } + + if txid == "" { + return false, false, errors.New("txid missing") + } + + if merkleRoot == "" { + return false, false, errors.New("merkleRoot missing") + } + + return verifyProof(txid, merkleRoot, proof.Index, proof.Nodes) +} + +func verifyProof(c, merkleRoot string, index uint64, nodes []string) (bool, bool, error) { + isLastInTree := true + + for _, p := range nodes { + // Check if the node is the left or the right child + cIsLeft := index%2 == 0 + + // Check for duplicate hash - this happens if the node (p) is + // the last element of an uneven merkle tree layer + if p == "*" { + if !cIsLeft { // this shouldn't happen... + return false, false, ErrInvalidNodes + } + p = c + } + + // This check fails at least once if it's not the last element + if cIsLeft && c != p { + isLastInTree = false + } + + var err error + // Calculate the parent node + if cIsLeft { + // Concatenate left leaf (c) with right leaf (p) + c, err = bc.MerkleTreeParentStr(c, p) + if err != nil { + return false, false, err + } + } else { + // Concatenate left leaf (p) with right leaf (c) + c, err = bc.MerkleTreeParentStr(p, c) + if err != nil { + return false, false, err + } + } + + // We need integer division here with remainder dropped. + index = index / 2 + } + + // c is now the calculated merkle root + return c == merkleRoot, isLastInTree, nil +} + +func validateTxOrID(flags byte, txOrID string) error { + // The `txOrId` field contains a full transaction + if len(txOrID) > 64 && flags&txOrIDFlag == 0 { + return errors.New("expecting txid but got tx") + } + + // The `txOrId` field contains a transaction ID + if len(txOrID) == 64 && flags&txOrIDFlag == 1 { + return errors.New("expecting tx but got txid") + } + + return nil +} + +func txidFromTxOrID(txOrID string) (string, error) { + + // The `txOrId` field contains a transaction ID + if len(txOrID) == 64 { + return txOrID, nil + } + + // The `txOrId` field contains a full transaction + if len(txOrID) > 64 { + tx, err := bt.NewTxFromString(txOrID) + if err != nil { + return "", err + } + + return tx.TxID(), nil + } + + return "", errors.New("invalid txOrId length - must be at least 64 chars (32 bytes)") +} + +type merkleProofBinary struct { + flags byte + index uint64 + txOrID string + target string + nodes []string +} + +func parseBinaryMerkleProof(proof []byte) (*merkleProofBinary, error) { + mpb := &merkleProofBinary{} + + var offset int + + // flags is first byte + mpb.flags = proof[offset] + offset++ + + // index is the next varint after the 1st byte + index, size := bt.NewVarIntFromBytes(proof[offset:]) + mpb.index = uint64(index) + offset += size + + var txLength bt.VarInt + // if bit 1 of flags is NOT set, txOrId should contain txid (= 32 bytes) + if mpb.flags&1 == 0 { + txLength = 32 + } + + // if bit 1 of flags is set, txOrId should contain tx hex (> 32 bytes) + if mpb.flags&1 == 1 { + // txLength is the next varint after the 1st byte + index size + txLength, size = bt.NewVarIntFromBytes(proof[offset:]) + offset += size + if txLength <= 32 { + return nil, errors.New("invalid tx length (should be greater than 32 bytes)") + } + } + + // txOrID is the next txLength bytes after 1st byte + index size (+ txLength size) + mpb.txOrID = hex.EncodeToString(bt.ReverseBytes(proof[offset : offset+int(txLength)])) + offset += int(txLength) + + switch mpb.flags & targetTypeFlags { + // if bits 1 and 2 of flags are NOT set, target should contain a block hash (32 bytes) + // if bit 2 of flags is set, target should contain a merkle root (32 bytes) + case 0, 4: + mpb.target = hex.EncodeToString(bt.ReverseBytes(proof[offset : offset+32])) + offset += 32 + + // if bit 1 of flags is set, target should contain a block header (80 bytes) + case 2: + mpb.target = hex.EncodeToString(bt.ReverseBytes(proof[offset : offset+80])) + offset += 80 + + default: + return nil, ErrInvalidMerkleFlags + } + + nodeCount, size := bt.NewVarIntFromBytes(proof[offset:]) + offset += size + + if mpb.index >= 1<