diff --git a/.github/auto_request_review.yml b/.github/auto_request_review.yml new file mode 100644 index 0000000..bc48a57 --- /dev/null +++ b/.github/auto_request_review.yml @@ -0,0 +1,15 @@ +reviewers: + defaults: + - rollkit + groups: + rollkit: + - team:core +files: + ".github/**": + - MSevey + - rollkit +options: + ignore_draft: true + ignored_keywords: + - WIP + number_of_reviewers: 3 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 87341e7..31d5bf9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,4 +26,3 @@ updates: applies-to: version-updates update-types: - "minor" - diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml new file mode 100644 index 0000000..c18bc03 --- /dev/null +++ b/.github/workflows/ci_release.yml @@ -0,0 +1,57 @@ +name: CI and Release +on: + push: + branches: + - main + # Trigger on version tags + tags: + - "v*" + pull_request: + merge_group: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + version: + # Friendly description to be shown in the UI instead of 'name' + description: "Semver type of new version (major / minor / patch)" + # Input has to be provided for the workflow to run + required: true + type: choice + options: + - patch + - minor + - major + +jobs: + lint: + uses: ./.github/workflows/lint.yml + + test: + uses: ./.github/workflows/test.yml + secrets: inherit + + # branch_name trims ref/heads/ from github.ref to access a clean branch name + branch_name: + runs-on: ubuntu-latest + outputs: + branch: ${{ steps.trim_ref.outputs.branch }} + steps: + - name: Trim branch name + id: trim_ref + run: | + echo "branch=$(${${{ github.ref }}:11})" >> $GITHUB_OUTPUT + + # Make a release if this is a manually trigger job, i.e. workflow_dispatch + release: + needs: [lint, test, branch_name] + runs-on: ubuntu-latest + if: ${{ github.event_name == 'workflow_dispatch' }} + permissions: "write-all" + steps: + - uses: actions/checkout@v4 + - name: Version Release + uses: rollkit/.github/.github/actions/version-release@v0.4.1 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + version-bump: ${{inputs.version}} + release-branch: ${{needs.branch_name.outputs.branch}} diff --git a/.github/workflows/housekeeping.yml b/.github/workflows/housekeeping.yml new file mode 100644 index 0000000..16d3904 --- /dev/null +++ b/.github/workflows/housekeeping.yml @@ -0,0 +1,66 @@ +name: Housekeeping + +on: + issues: + types: [opened] + pull_request_target: + types: [opened, ready_for_review] + +jobs: + issue-management: + if: ${{ github.event.issue }} + name: Add issues to project and add triage label + uses: rollkit/.github/.github/workflows/reusable_housekeeping.yml@v0.4.1 + secrets: inherit + permissions: + issues: write + pull-requests: write + with: + run-labels: true + labels-to-add: "needs-triage" + run-projects: true + project-url: https://github.com/orgs/rollkit/projects/7 + + add-pr-to-project: + # ignore dependabot PRs + if: ${{ github.event.pull_request && github.actor != 'dependabot[bot]' }} + name: Add PRs to project + uses: rollkit/.github/.github/workflows/reusable_housekeeping.yml@v0.4.1 + secrets: inherit + permissions: + issues: write + pull-requests: write + with: + run-projects: true + project-url: https://github.com/orgs/rollkit/projects/7 + + auto-add-reviewer: + name: Auto add reviewer to PR + if: github.event.pull_request + uses: rollkit/.github/.github/workflows/reusable_housekeeping.yml@v0.4.1 + secrets: inherit + permissions: + issues: write + pull-requests: write + with: + run-auto-request-review: true + + auto-add-assignee: + # ignore dependabot PRs + if: ${{ github.event.pull_request && github.actor != 'dependabot[bot]' }} + name: Assign issue and PR to creator + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Set pull_request url and creator login + # yamllint disable rule:line-length + run: | + echo "PR=${{ github.event.pull_request.html_url }}" >> $GITHUB_ENV + echo "CREATOR=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV + # yamllint enable rule:line-length + - name: Assign PR to creator + run: gh pr edit ${{ env.PR }} --add-assignee ${{ env.CREATOR }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..aa80647 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,43 @@ +# lint runs all linters in this repository +# This workflow is triggered by ci_release.yml workflow +name: lint +on: + workflow_call: + +jobs: + golangci-lint: + name: golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + # This steps sets the GIT_DIFF environment variable to true + # if files defined in PATTERS changed + - uses: technote-space/get-diff-action@v6.1.2 + with: + # This job will pass without running if go.mod, go.sum, and *.go + # wasn't modified. + PATTERNS: | + **/**.go + go.mod + go.sum + - uses: golangci/golangci-lint-action@v6.1.0 + with: + version: latest + args: --timeout 10m + github-token: ${{ secrets.github_token }} + if: env.GIT_DIFF + + yamllint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: rollkit/.github/.github/actions/yamllint@v0.4.1 + + markdown-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: rollkit/.github/.github/actions/markdown-lint@v0.4.1 diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml new file mode 100644 index 0000000..e11fe30 --- /dev/null +++ b/.github/workflows/semantic-pull-request.yml @@ -0,0 +1,20 @@ +name: Semantic Pull Request + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + main: + name: conventional-commit-pr-title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..bb6d88f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,47 @@ +# Tests / Code Coverage workflow +# This workflow is triggered by ci_release.yml workflow +name: Tests / Code Coverage +on: + workflow_call: + +jobs: + go_mod_tidy_check: + name: Go Mod Tidy Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + - run: go mod tidy + - name: check for diff + run: git diff --exit-code + + unit_test: + name: Run Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + - name: Run unit test + run: go test -v -race -covermode=atomic -coverprofile=coverage.txt + - name: upload coverage report + uses: codecov/codecov-action@v4.5.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.txt + + integration_test: + name: Run Integration Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: set up go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + - name: Integration Tests + run: echo "No integration tests yet" diff --git a/.gitignore b/.gitignore index fc8abcf..776a243 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .btcdeb_history +coverage.txt diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..6017061 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,46 @@ +run: + timeout: 5m + modules-download-mode: readonly + +linters: + enable: + - errorlint + - errcheck + - gofmt + - goimports + - gosec + - gosimple + - govet + - ineffassign + - misspell + - revive + - staticcheck + - typecheck + - unconvert + - unused + +issues: + exclude-use-default: false + # mempool and indexer code is borrowed from Tendermint + exclude-dirs: + - mempool + - state/indexer + - state/txindex + - third_party + include: + - EXC0012 # EXC0012 revive: Annoying issue about not having a comment. The rare codebase has such comments + - EXC0014 # EXC0014 revive: Annoying issue about not having a comment. The rare codebase has such comments + +linters-settings: + revive: + rules: + - name: package-comments + disabled: true + - name: duplicated-imports + severity: warning + - name: exported + arguments: + - disableStutteringCheck + + goimports: + local-prefixes: github.com/rollkit diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..6369b8d --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,6 @@ +default: true +MD010: + code_blocks: false +MD013: false +MD024: + allow_different_nesting: true diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..cd2a9e8 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +--- +# Built from docs https://yamllint.readthedocs.io/en/stable/configuration.html +extends: default + +rules: + # 120 chars should be enough, but don't fail if a line is longer + line-length: + max: 120 + level: warning diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d313e5b --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +DOCKER := $(shell which docker) +DOCKER_BUF := $(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace bufbuild/buf +PACKAGE_NAME := github.com/rollkit/rollkit +GOLANG_CROSS_VERSION ?= v1.22.1 + +# Define pkgs, run, and cover variables for test so that we can override them in +# the terminal more easily. + +# IGNORE_DIRS is a list of directories to ignore when running tests and linters. +# This list is space separated. +pkgs := $(shell go list ./...) +run := . +count := 1 + +## help: Show this help message +help: Makefile + @echo " Choose a command run in "$(PROJECTNAME)":" + @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' +.PHONY: help + +## clean: clean testcache +clean: + @echo "--> Clearing testcache" + @go clean --testcache +.PHONY: clean + +## cover: generate to code coverage report. +cover: + @echo "--> Generating Code Coverage" + @go install github.com/ory/go-acc@latest + @go-acc -o coverage.txt $(pkgs) +.PHONY: cover + +## deps: Install dependencies +deps: + @echo "--> Installing dependencies" + @go mod download + @go mod tidy +.PHONY: deps + +## lint: Run linters golangci-lint and markdownlint. +lint: vet + @echo "--> Running golangci-lint" + @golangci-lint run + @echo "--> Running markdownlint" + @markdownlint --config .markdownlint.yaml --ignore './cmd/rollkit/docs/*.md' '**/*.md' + @echo "--> Running yamllint" + @yamllint --no-warnings . -c .yamllint.yml + @echo "--> Running actionlint" + @actionlint + +.PHONY: lint + +## fmt: Run fixes for linters. +fmt: + @echo "--> Formatting markdownlint" + @markdownlint --config .markdownlint.yaml --ignore './cmd/rollkit/docs/*.md' '**/*.md' -f + @echo "--> Formatting go" + @golangci-lint run --fix +.PHONY: fmt + +## vet: Run go vet +vet: + @echo "--> Running go vet" + @go vet $(pkgs) +.PHONY: vet + +## test: Running unit tests +test: vet + @echo "--> Running unit tests" + @go test -v -race -covermode=atomic -coverprofile=coverage.txt $(pkgs) -run $(run) -count=$(count) +.PHONY: test diff --git a/README.md b/README.md index e09190c..532dc60 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,8 @@ -bitcoin-da: -=========== - +# bitcoin-da This package provides a reader / writer interface to bitcoin. -Example: -======== +## Example // ExampleRelayer_Read tests that reading data from the blockchain works as // expected. @@ -46,8 +43,7 @@ Example: // Output: rollkit-btc: gm } -Tests: -====== +## Tests Running the tests requires a local regtest node. @@ -68,8 +64,7 @@ Idle for a while till coinbase coins mature. PASS ok github.com/rollkit/bitcoin-da 0.375s -Writer: -======= +## Writer A commit transaction containing a taproot with one leaf script @@ -83,21 +78,16 @@ A commit transaction containing a taproot with one leaf script is used to create a new bech32m address and is sent an output. - A reveal transaction then posts the embedded data on chain and spends the commit output. - -Reader: -======== +## Reader The address of the reveal transaction is implicity used as a namespace. - Clients may call listunspent on the reveal transaction address to get a list of transactions and read the embedded data from the first witness input. -Spec: -===== +## Spec For more details, [read the spec](./spec.md) diff --git a/bitcoin.go b/bitcoin.go index 0d9faa6..e07a346 100644 --- a/bitcoin.go +++ b/bitcoin.go @@ -4,19 +4,23 @@ import ( "context" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/rollkit/go-da" ) -const DefaultMaxBytes = 1048576; +// DefaultMaxBytes is the default max blob size +const DefaultMaxBytes = 1048576 +// BitcoinDA is a Data Availability layer for Bitcoin. type BitcoinDA struct { - relayer *Relayer + relayer *Relayer } +// NewBitcoinDA creates a new BitcoinDA. func NewBitcoinDA(relayer *Relayer) *BitcoinDA { - return &BitcoinDA{ - relayer: relayer, - } + return &BitcoinDA{ + relayer: relayer, + } } // MaxBlobSize returns the max blob size @@ -26,22 +30,21 @@ func (b *BitcoinDA) MaxBlobSize(ctx context.Context) (uint64, error) { // Get returns Blob for each given ID, or an error. func (b *BitcoinDA) Get(ctx context.Context, ids []da.ID, ns da.Namespace) ([]da.Blob, error) { - var blobs []da.Blob - for _, id := range ids { - hash, err := chainhash.NewHash(id) - if err != nil { - return nil, err - } - blob, err := b.relayer.ReadTransaction(hash) - if err != nil { - return nil, err - } - blobs = append(blobs, blob) - } - return blobs, nil + var blobs []da.Blob + for _, id := range ids { + hash, err := chainhash.NewHash(id) + if err != nil { + return nil, err + } + blob, err := b.relayer.ReadTransaction(hash) + if err != nil { + return nil, err + } + blobs = append(blobs, blob) + } + return blobs, nil } - // Commit creates a Commitment for each given Blob. func (b *BitcoinDA) Commit(ctx context.Context, daBlobs []da.Blob, ns da.Namespace) ([]da.Commitment, error) { // not implemented @@ -64,11 +67,11 @@ func (b *BitcoinDA) GetProofs(ctx context.Context, daIDs []da.ID, ns da.Namespac func (b *BitcoinDA) Submit(ctx context.Context, daBlobs []da.Blob, gasPrice float64, ns da.Namespace) ([]da.ID, error) { var ids []da.ID for _, blob := range daBlobs { - hash, err := b.relayer.Write(blob) - if err != nil { - return nil, err - } - ids = append(ids, hash.CloneBytes()) + hash, err := b.relayer.Write(blob) + if err != nil { + return nil, err + } + ids = append(ids, hash.CloneBytes()) } return ids, nil } diff --git a/relayer.go b/relayer.go index 4566b11..c59aa92 100644 --- a/relayer.go +++ b/relayer.go @@ -49,7 +49,7 @@ func chunkSlice(slice []byte, chunkSize int) [][]byte { func createTaprootAddress(embeddedData []byte) (string, error) { privKey, err := btcutil.DecodeWIF(bobPrivateKey) if err != nil { - return "", fmt.Errorf("error decoding bob private key: %v", err) + return "", fmt.Errorf("error decoding bob private key: %w", err) } pubKey := privKey.PrivKey.PubKey() @@ -67,7 +67,7 @@ func createTaprootAddress(embeddedData []byte) (string, error) { builder.AddOp(txscript.OP_CHECKSIG) pkScript, err := builder.Script() if err != nil { - return "", fmt.Errorf("error building script: %v", err) + return "", fmt.Errorf("error building script: %w", err) } tapLeaf := txscript.NewBaseTapLeaf(pkScript) @@ -75,7 +75,7 @@ func createTaprootAddress(embeddedData []byte) (string, error) { internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey) if err != nil { - return "", fmt.Errorf("error decoding internal private key: %v", err) + return "", fmt.Errorf("error decoding internal private key: %w", err) } internalPubKey := internalPrivKey.PrivKey.PubKey() @@ -90,7 +90,7 @@ func createTaprootAddress(embeddedData []byte) (string, error) { address, err := btcutil.NewAddressTaproot( schnorr.SerializePubKey(outputKey), &chaincfg.RegressionNetParams) if err != nil { - return "", fmt.Errorf("error encoding Taproot address: %v", err) + return "", fmt.Errorf("error encoding Taproot address: %w", err) } return address.String(), nil @@ -111,7 +111,7 @@ type Relayer struct { } // close shuts down the client. -func (r Relayer) close() { +func (r Relayer) close() { // nolint:unused r.client.Shutdown() } @@ -123,17 +123,17 @@ func (r Relayer) commitTx(addr string) (*chainhash.Hash, error) { // Create a transaction that sends 0.001 BTC to the given address. address, err := btcutil.DecodeAddress(addr, &chaincfg.RegressionNetParams) if err != nil { - return nil, fmt.Errorf("error decoding recipient address: %v", err) + return nil, fmt.Errorf("error decoding recipient address: %w", err) } amount, err := btcutil.NewAmount(0.001) if err != nil { - return nil, fmt.Errorf("error creating new amount: %v", err) + return nil, fmt.Errorf("error creating new amount: %w", err) } hash, err := r.client.SendToAddress(address, amount) if err != nil { - return nil, fmt.Errorf("error sending to address: %v", err) + return nil, fmt.Errorf("error sending to address: %w", err) } return hash, nil @@ -145,7 +145,7 @@ func (r Relayer) commitTx(addr string) (*chainhash.Hash, error) { func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*chainhash.Hash, error) { rawCommitTx, err := r.client.GetRawTransaction(commitHash) if err != nil { - return nil, fmt.Errorf("error getting raw commit tx: %v", err) + return nil, fmt.Errorf("error getting raw commit tx: %w", err) } // TODO: use a better way to find our output @@ -161,14 +161,14 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha privKey, err := btcutil.DecodeWIF(bobPrivateKey) if err != nil { - return nil, fmt.Errorf("error decoding bob private key: %v", err) + return nil, fmt.Errorf("error decoding bob private key: %w", err) } pubKey := privKey.PrivKey.PubKey() internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey) if err != nil { - return nil, fmt.Errorf("error decoding internal private key: %v", err) + return nil, fmt.Errorf("error decoding internal private key: %w", err) } internalPubKey := internalPrivKey.PrivKey.PubKey() @@ -187,7 +187,7 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha builder.AddOp(txscript.OP_CHECKSIG) pkScript, err := builder.Script() if err != nil { - return nil, fmt.Errorf("error building script: %v", err) + return nil, fmt.Errorf("error building script: %w", err) } tapLeaf := txscript.NewBaseTapLeaf(pkScript) @@ -203,14 +203,14 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha ) p2trScript, err := payToTaprootScript(outputKey) if err != nil { - return nil, fmt.Errorf("error building p2tr script: %v", err) + return nil, fmt.Errorf("error building p2tr script: %w", err) } tx := wire.NewMsgTx(2) tx.AddTxIn(&wire.TxIn{ PreviousOutPoint: wire.OutPoint{ Hash: *rawCommitTx.Hash(), - Index: uint32(commitIndex), + Index: uint32(commitIndex), // nolint:gosec }, }) txOut := &wire.TxOut{ @@ -231,14 +231,14 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha ) if err != nil { - return nil, fmt.Errorf("error signing tapscript: %v", err) + return nil, fmt.Errorf("error signing tapscript: %w", err) } // Now that we have the sig, we'll make a valid witness // including the control block. ctrlBlockBytes, err := ctrlBlock.ToBytes() if err != nil { - return nil, fmt.Errorf("error including control block: %v", err) + return nil, fmt.Errorf("error including control block: %w", err) } tx.TxIn[0].Witness = wire.TxWitness{ sig, pkScript, ctrlBlockBytes, @@ -246,11 +246,12 @@ func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*cha hash, err := r.client.SendRawTransaction(tx, false) if err != nil { - return nil, fmt.Errorf("error sending reveal transaction: %v", err) + return nil, fmt.Errorf("error sending reveal transaction: %w", err) } return hash, nil } +// Config is the relayer config type Config struct { Host string User string @@ -274,13 +275,14 @@ func NewRelayer(config Config) (*Relayer, error) { } client, err := rpcclient.New(connCfg, nil) if err != nil { - return nil, fmt.Errorf("error creating btcd RPC client: %v", err) + return nil, fmt.Errorf("error creating btcd RPC client: %w", err) } return &Relayer{ client: client, }, nil } +// ReadTransaction reads a transaction from the blockchain given a hash. func (r Relayer) ReadTransaction(hash *chainhash.Hash) ([]byte, error) { tx, err := r.client.GetRawTransaction(hash) if err != nil { @@ -301,7 +303,7 @@ func (r Relayer) ReadTransaction(hash *chainhash.Hash) ([]byte, error) { } func (r Relayer) Read(height uint64) ([][]byte, error) { - hash, err := r.client.GetBlockHash(int64(height)) + hash, err := r.client.GetBlockHash(int64(height)) // nolint:gosec if err != nil { return nil, err } @@ -344,6 +346,7 @@ func (r Relayer) Write(data []byte) (*chainhash.Hash, error) { return hash, nil } +// ExtractPushData ... func ExtractPushData(version uint16, pkScript []byte) ([]byte, error) { type templateMatch struct { expectPushData bool diff --git a/spec.md b/spec.md index 62ba712..e4d3ea3 100644 --- a/spec.md +++ b/spec.md @@ -13,7 +13,7 @@ Define a common read and write path that can be used by rollup development kits ## How? -Ordinals developer Casey Rodarmor found a way to essentially create the equivalent of “calldata” on bitcoin script thanks to the Taproot upgrade. Additionally, this “calldata” can be as large as the bitcoin block size limit (4MB), benefits from the SegWit discount, making “blobspace” cheaper and more abundant than on Ethereum as of February 20, 2023. +Ordinals developer Casey Rodarmor found a way to essentially create the equivalent of “calldata” on bitcoin script thanks to the Taproot upgrade. Additionally, this “calldata” can be as large as the bitcoin block size limit (4MB), benefits from the SegWit discount, making “blobspace” cheaper and more abundant than on Ethereum as of February 20, 2023. For the purpose of writing data to Bitcoin in order to use it as a DA layer, we will be [inscribing](https://docs.ordinals.com/inscriptions.html) our block data. @@ -24,7 +24,7 @@ In order to inscribe our data into a satoshi, we need to use Taproot scripts, si 1. First, in the commit transaction, a taproot output committing to a script containing the inscription content is created. 2. Second, in the reveal transaction, the output created by the commit transaction is spent, revealing the inscription content on-chain. -But what about the actual data: +But what about the actual data: “Inscription content is serialized using data pushes within unexecuted conditionals, called an "envelopes". Envelopes consist of an `OP_FALSE OP_IF … OP_ENDIF`  wrapping any number of data pushes. Because envelopes are effectively no-ops, they do not change the semantics of the script in which they are included, and can be combined with any other locking script.” @@ -129,4 +129,4 @@ Once we have found the utxo with the data for a given block, we can call `gettra } ``` -Finally, in the `decoded` field, we can get the witness data by reading the `witness` field for the first input of the decoded transaction. We can then parse our witness data and read our block data accordingly. \ No newline at end of file +Finally, in the `decoded` field, we can get the witness data by reading the `witness` field for the first input of the decoded transaction. We can then parse our witness data and read our block data accordingly.