├── .gitignore ├── NOTICE ├── .markdownlint.yaml ├── .yamllint.yml ├── .github ├── auto_request_review.yml ├── workflows │ ├── semantic-pull-request.yml │ ├── semantic_release.yml │ ├── lint.yml │ ├── test.yml │ ├── ci_release.yml │ └── housekeeping.yml └── dependabot.yml ├── go.mod ├── .golangci.yml ├── relayer_test.go ├── Makefile ├── bitcoin.go ├── README.md ├── spec.md ├── go.sum ├── LICENSE └── relayer.go /.gitignore: -------------------------------------------------------------------------------- 1 | .btcdeb_history 2 | coverage.txt 3 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Bitcoin DA 2 | Copyright 2023 and onwards Strange Loop Labs AG 3 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | default: true 2 | MD010: 3 | code_blocks: false 4 | MD013: false 5 | MD024: 6 | allow_different_nesting: true 7 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Built from docs https://yamllint.readthedocs.io/en/stable/configuration.html 3 | extends: default 4 | 5 | rules: 6 | # 120 chars should be enough, but don't fail if a line is longer 7 | line-length: 8 | max: 120 9 | level: warning 10 | -------------------------------------------------------------------------------- /.github/auto_request_review.yml: -------------------------------------------------------------------------------- 1 | reviewers: 2 | defaults: 3 | - rollkit 4 | groups: 5 | rollkit: 6 | - team:core 7 | files: 8 | ".github/**": 9 | - MSevey 10 | - rollkit 11 | options: 12 | ignore_draft: true 13 | ignored_keywords: 14 | - WIP 15 | number_of_reviewers: 3 16 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Pull Request 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: read 12 | 13 | jobs: 14 | main: 15 | name: conventional-commit-pr-title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v5 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - T:dependencies 10 | # Group all patch updates into a single PR 11 | groups: 12 | patch-updates: 13 | applies-to: version-updates 14 | update-types: 15 | - "patch" 16 | - package-ecosystem: gomod 17 | directory: "/" 18 | schedule: 19 | interval: daily 20 | open-pull-requests-limit: 10 21 | labels: 22 | - T:dependencies 23 | # Group all patch updates into a single PR 24 | groups: 25 | patch-updates: 26 | applies-to: version-updates 27 | update-types: 28 | - "patch" 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rollkit/bitcoin-da 2 | 3 | go 1.21.1 4 | 5 | require ( 6 | github.com/btcsuite/btcd v0.23.4 7 | github.com/btcsuite/btcd/btcec/v2 v2.1.3 8 | github.com/btcsuite/btcd/btcutil v1.1.3 9 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 10 | ) 11 | 12 | require ( 13 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 14 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 15 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 16 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect 17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 18 | github.com/rollkit/go-da v0.5.0 19 | golang.org/x/crypto v0.17.0 // indirect 20 | golang.org/x/sys v0.15.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /.github/workflows/semantic_release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Configure Semantic Release 15 | # Work around for non npm project 16 | # REF: https://github.com/cycjimmy/semantic-release-action/issues/115#issuecomment-1817264419 17 | run: echo '{"branches":[],"plugins":["@semantic-release/commit-analyzer","@semantic-release/release-notes-generator","@semantic-release/github"]}' > .releaserc.json 18 | - name: Create Release 19 | uses: cycjimmy/semantic-release-action@v4 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | with: 23 | branches: | 24 | ["main"] 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | modules-download-mode: readonly 4 | 5 | linters: 6 | enable: 7 | - errorlint 8 | - errcheck 9 | - gofmt 10 | - goimports 11 | - gosec 12 | - gosimple 13 | - govet 14 | - ineffassign 15 | - misspell 16 | - revive 17 | - staticcheck 18 | - typecheck 19 | - unconvert 20 | - unused 21 | 22 | issues: 23 | exclude-use-default: false 24 | # mempool and indexer code is borrowed from Tendermint 25 | exclude-dirs: 26 | - mempool 27 | - state/indexer 28 | - state/txindex 29 | - third_party 30 | include: 31 | - EXC0012 # EXC0012 revive: Annoying issue about not having a comment. The rare codebase has such comments 32 | - EXC0014 # EXC0014 revive: Annoying issue about not having a comment. The rare codebase has such comments 33 | 34 | linters-settings: 35 | revive: 36 | rules: 37 | - name: package-comments 38 | disabled: true 39 | - name: duplicated-imports 40 | severity: warning 41 | - name: exported 42 | arguments: 43 | - disableStutteringCheck 44 | 45 | goimports: 46 | local-prefixes: github.com/rollkit 47 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # lint runs all linters in this repository 2 | # This workflow is triggered by ci_release.yml workflow 3 | name: lint 4 | on: 5 | workflow_call: 6 | 7 | jobs: 8 | golangci-lint: 9 | name: golangci-lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version-file: ./go.mod 16 | # This steps sets the GIT_DIFF environment variable to true 17 | # if files defined in PATTERS changed 18 | - uses: technote-space/get-diff-action@v6.1.2 19 | with: 20 | # This job will pass without running if go.mod, go.sum, and *.go 21 | # wasn't modified. 22 | PATTERNS: | 23 | **/**.go 24 | go.mod 25 | go.sum 26 | - uses: golangci/golangci-lint-action@v6.1.1 27 | with: 28 | version: latest 29 | args: --timeout 10m 30 | github-token: ${{ secrets.github_token }} 31 | if: env.GIT_DIFF 32 | 33 | yamllint: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: rollkit/.github/.github/actions/yamllint@v0.4.1 38 | 39 | markdown-lint: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: rollkit/.github/.github/actions/markdown-lint@v0.4.1 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Tests / Code Coverage workflow 2 | # This workflow is triggered by ci_release.yml workflow 3 | name: Tests / Code Coverage 4 | on: 5 | workflow_call: 6 | 7 | jobs: 8 | go_mod_tidy_check: 9 | name: Go Mod Tidy Check 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version-file: ./go.mod 16 | - run: go mod tidy 17 | - name: check for diff 18 | run: git diff --exit-code 19 | 20 | unit_test: 21 | name: Run Unit Tests 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: set up go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version-file: ./go.mod 29 | - name: Run unit test 30 | run: go test -v -race -covermode=atomic -coverprofile=coverage.txt 31 | - name: upload coverage report 32 | uses: codecov/codecov-action@v4.6.0 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | file: ./coverage.txt 36 | 37 | integration_test: 38 | name: Run Integration Tests 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: set up go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version-file: ./go.mod 46 | - name: Integration Tests 47 | run: echo "No integration tests yet" 48 | -------------------------------------------------------------------------------- /.github/workflows/ci_release.yml: -------------------------------------------------------------------------------- 1 | name: CI and Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | # Trigger on version tags 7 | tags: 8 | - "v*" 9 | pull_request: 10 | merge_group: 11 | workflow_dispatch: 12 | # Inputs the workflow accepts. 13 | inputs: 14 | version: 15 | # Friendly description to be shown in the UI instead of 'name' 16 | description: "Semver type of new version (major / minor / patch)" 17 | # Input has to be provided for the workflow to run 18 | required: true 19 | type: choice 20 | options: 21 | - patch 22 | - minor 23 | - major 24 | 25 | jobs: 26 | lint: 27 | uses: ./.github/workflows/lint.yml 28 | 29 | test: 30 | uses: ./.github/workflows/test.yml 31 | secrets: inherit 32 | 33 | # branch_name trims ref/heads/ from github.ref to access a clean branch name 34 | branch_name: 35 | runs-on: ubuntu-latest 36 | outputs: 37 | branch: ${{ steps.trim_ref.outputs.branch }} 38 | steps: 39 | - name: Trim branch name 40 | id: trim_ref 41 | run: | 42 | echo "branch=$(${${{ github.ref }}:11})" >> $GITHUB_OUTPUT 43 | 44 | # Make a release if this is a manually trigger job, i.e. workflow_dispatch 45 | release: 46 | needs: [lint, test, branch_name] 47 | runs-on: ubuntu-latest 48 | if: ${{ github.event_name == 'workflow_dispatch' }} 49 | permissions: "write-all" 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Version Release 53 | uses: rollkit/.github/.github/actions/version-release@v0.4.1 54 | with: 55 | github-token: ${{secrets.GITHUB_TOKEN}} 56 | version-bump: ${{inputs.version}} 57 | release-branch: ${{needs.branch_name.outputs.branch}} 58 | -------------------------------------------------------------------------------- /relayer_test.go: -------------------------------------------------------------------------------- 1 | package bitcoinda_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | bitcoinda "github.com/rollkit/bitcoin-da" 8 | ) 9 | 10 | // ExampleRelayer_Write tests that writing data to the blockchain works as 11 | // expected. 12 | func ExampleRelayer_Write() { 13 | // Example usage 14 | relayer, err := bitcoinda.NewRelayer(bitcoinda.Config{ 15 | Host: "localhost:18332", 16 | User: "rpcuser", 17 | Pass: "rpcpass", 18 | HTTPPostMode: true, 19 | DisableTLS: true, 20 | }) 21 | if err != nil { 22 | fmt.Println(err) 23 | return 24 | } 25 | fmt.Println("Writing...") 26 | _, err = relayer.Write([]byte("rollkit-btc: gm")) 27 | if err != nil { 28 | fmt.Println(err) 29 | return 30 | } 31 | fmt.Println("done") 32 | // Output: Writing... 33 | // done 34 | } 35 | 36 | // ExampleRelayer_Read tests that reading data from the blockchain works as 37 | // expected. 38 | func ExampleRelayer_Read() { 39 | // Example usage 40 | relayer, err := bitcoinda.NewRelayer(bitcoinda.Config{ 41 | Host: "localhost:18332", 42 | User: "rpcuser", 43 | Pass: "rpcpass", 44 | HTTPPostMode: true, 45 | DisableTLS: true, 46 | }) 47 | if err != nil { 48 | fmt.Println(err) 49 | return 50 | } 51 | _, err = relayer.Write([]byte("rollkit-btc: gm")) 52 | if err != nil { 53 | fmt.Println(err) 54 | return 55 | } 56 | // TODO: either mock or generate block 57 | // We're assuming the prev tx was mined at height 146 58 | height := uint64(146) 59 | blobs, err := relayer.Read(height) 60 | if err != nil { 61 | fmt.Println(err) 62 | return 63 | } 64 | for _, blob := range blobs { 65 | got, err := hex.DecodeString(fmt.Sprintf("%x", blob)) 66 | if err != nil { 67 | fmt.Println(err) 68 | return 69 | } 70 | fmt.Println(string(got)) 71 | } 72 | // Output: rollkit-btc: gm 73 | } 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER := $(shell which docker) 2 | DOCKER_BUF := $(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace bufbuild/buf 3 | PACKAGE_NAME := github.com/rollkit/rollkit 4 | GOLANG_CROSS_VERSION ?= v1.22.1 5 | 6 | # Define pkgs, run, and cover variables for test so that we can override them in 7 | # the terminal more easily. 8 | 9 | # IGNORE_DIRS is a list of directories to ignore when running tests and linters. 10 | # This list is space separated. 11 | pkgs := $(shell go list ./...) 12 | run := . 13 | count := 1 14 | 15 | ## help: Show this help message 16 | help: Makefile 17 | @echo " Choose a command run in "$(PROJECTNAME)":" 18 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 19 | .PHONY: help 20 | 21 | ## clean: clean testcache 22 | clean: 23 | @echo "--> Clearing testcache" 24 | @go clean --testcache 25 | .PHONY: clean 26 | 27 | ## cover: generate to code coverage report. 28 | cover: 29 | @echo "--> Generating Code Coverage" 30 | @go install github.com/ory/go-acc@latest 31 | @go-acc -o coverage.txt $(pkgs) 32 | .PHONY: cover 33 | 34 | ## deps: Install dependencies 35 | deps: 36 | @echo "--> Installing dependencies" 37 | @go mod download 38 | @go mod tidy 39 | .PHONY: deps 40 | 41 | ## lint: Run linters golangci-lint and markdownlint. 42 | lint: vet 43 | @echo "--> Running golangci-lint" 44 | @golangci-lint run 45 | @echo "--> Running markdownlint" 46 | @markdownlint --config .markdownlint.yaml --ignore './cmd/rollkit/docs/*.md' '**/*.md' 47 | @echo "--> Running yamllint" 48 | @yamllint --no-warnings . -c .yamllint.yml 49 | @echo "--> Running actionlint" 50 | @actionlint 51 | 52 | .PHONY: lint 53 | 54 | ## fmt: Run fixes for linters. 55 | fmt: 56 | @echo "--> Formatting markdownlint" 57 | @markdownlint --config .markdownlint.yaml --ignore './cmd/rollkit/docs/*.md' '**/*.md' -f 58 | @echo "--> Formatting go" 59 | @golangci-lint run --fix 60 | .PHONY: fmt 61 | 62 | ## vet: Run go vet 63 | vet: 64 | @echo "--> Running go vet" 65 | @go vet $(pkgs) 66 | .PHONY: vet 67 | 68 | ## test: Running unit tests 69 | test: vet 70 | @echo "--> Running unit tests" 71 | @go test -v -race -covermode=atomic -coverprofile=coverage.txt $(pkgs) -run $(run) -count=$(count) 72 | .PHONY: test 73 | -------------------------------------------------------------------------------- /.github/workflows/housekeeping.yml: -------------------------------------------------------------------------------- 1 | name: Housekeeping 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | pull_request_target: 7 | types: [opened, ready_for_review] 8 | 9 | jobs: 10 | issue-management: 11 | if: ${{ github.event.issue }} 12 | name: Add issues to project and add triage label 13 | uses: rollkit/.github/.github/workflows/reusable_housekeeping.yml@v0.4.1 14 | secrets: inherit 15 | permissions: 16 | issues: write 17 | pull-requests: write 18 | with: 19 | run-labels: true 20 | labels-to-add: "needs-triage" 21 | run-projects: true 22 | project-url: https://github.com/orgs/rollkit/projects/7 23 | 24 | add-pr-to-project: 25 | # ignore dependabot PRs 26 | if: ${{ github.event.pull_request && github.actor != 'dependabot[bot]' }} 27 | name: Add PRs to project 28 | uses: rollkit/.github/.github/workflows/reusable_housekeeping.yml@v0.4.1 29 | secrets: inherit 30 | permissions: 31 | issues: write 32 | pull-requests: write 33 | with: 34 | run-projects: true 35 | project-url: https://github.com/orgs/rollkit/projects/7 36 | 37 | auto-add-reviewer: 38 | name: Auto add reviewer to PR 39 | if: github.event.pull_request 40 | uses: rollkit/.github/.github/workflows/reusable_housekeeping.yml@v0.4.1 41 | secrets: inherit 42 | permissions: 43 | issues: write 44 | pull-requests: write 45 | with: 46 | run-auto-request-review: true 47 | 48 | auto-add-assignee: 49 | # ignore dependabot PRs 50 | if: ${{ github.event.pull_request && github.actor != 'dependabot[bot]' }} 51 | name: Assign issue and PR to creator 52 | runs-on: ubuntu-latest 53 | permissions: 54 | issues: write 55 | pull-requests: write 56 | steps: 57 | - name: Set pull_request url and creator login 58 | # yamllint disable rule:line-length 59 | run: | 60 | echo "PR=${{ github.event.pull_request.html_url }}" >> $GITHUB_ENV 61 | echo "CREATOR=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV 62 | # yamllint enable rule:line-length 63 | - name: Assign PR to creator 64 | run: gh pr edit ${{ env.PR }} --add-assignee ${{ env.CREATOR }} 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | -------------------------------------------------------------------------------- /bitcoin.go: -------------------------------------------------------------------------------- 1 | package bitcoinda 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/btcsuite/btcd/chaincfg/chainhash" 7 | 8 | "github.com/rollkit/go-da" 9 | ) 10 | 11 | // DefaultMaxBytes is the default max blob size 12 | const DefaultMaxBytes = 1048576 13 | 14 | // BitcoinDA is a Data Availability layer for Bitcoin. 15 | type BitcoinDA struct { 16 | relayer *Relayer 17 | } 18 | 19 | // NewBitcoinDA creates a new BitcoinDA. 20 | func NewBitcoinDA(relayer *Relayer) *BitcoinDA { 21 | return &BitcoinDA{ 22 | relayer: relayer, 23 | } 24 | } 25 | 26 | // MaxBlobSize returns the max blob size 27 | func (b *BitcoinDA) MaxBlobSize(ctx context.Context) (uint64, error) { 28 | return DefaultMaxBytes, nil 29 | } 30 | 31 | // Get returns Blob for each given ID, or an error. 32 | func (b *BitcoinDA) Get(ctx context.Context, ids []da.ID, ns da.Namespace) ([]da.Blob, error) { 33 | var blobs []da.Blob 34 | for _, id := range ids { 35 | hash, err := chainhash.NewHash(id) 36 | if err != nil { 37 | return nil, err 38 | } 39 | blob, err := b.relayer.ReadTransaction(hash) 40 | if err != nil { 41 | return nil, err 42 | } 43 | blobs = append(blobs, blob) 44 | } 45 | return blobs, nil 46 | } 47 | 48 | // Commit creates a Commitment for each given Blob. 49 | func (b *BitcoinDA) Commit(ctx context.Context, daBlobs []da.Blob, ns da.Namespace) ([]da.Commitment, error) { 50 | // not implemented 51 | return nil, nil 52 | } 53 | 54 | // GetIDs returns IDs of all Blobs located in DA at given height. 55 | func (b *BitcoinDA) GetIDs(ctx context.Context, height uint64, ns da.Namespace) ([]da.ID, error) { 56 | // not implemented 57 | return nil, nil 58 | } 59 | 60 | // GetProofs returns the inclusion proofs for the given IDs. 61 | func (b *BitcoinDA) GetProofs(ctx context.Context, daIDs []da.ID, ns da.Namespace) ([]da.Proof, error) { 62 | // not implemented 63 | return nil, nil 64 | } 65 | 66 | // Submit submits the Blobs to Data Availability layer. 67 | func (b *BitcoinDA) Submit(ctx context.Context, daBlobs []da.Blob, gasPrice float64, ns da.Namespace) ([]da.ID, error) { 68 | var ids []da.ID 69 | for _, blob := range daBlobs { 70 | hash, err := b.relayer.Write(blob) 71 | if err != nil { 72 | return nil, err 73 | } 74 | ids = append(ids, hash.CloneBytes()) 75 | } 76 | return ids, nil 77 | } 78 | 79 | // Validate validates Commitments against the corresponding Proofs. This should be possible without retrieving the Blobs. 80 | func (b *BitcoinDA) Validate(ctx context.Context, ids []da.ID, daProofs []da.Proof, ns da.Namespace) ([]bool, error) { 81 | // not implemented 82 | return nil, nil 83 | } 84 | 85 | var _ da.DA = &BitcoinDA{} 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitcoin-da 2 | 3 | This package provides a reader / writer interface to bitcoin. 4 | 5 | ## Example 6 | 7 | // ExampleRelayer_Read tests that reading data from the blockchain works as 8 | // expected. 9 | func ExampleRelayer_Read() { 10 | // Example usage 11 | relayer, err := bitcoinda.NewRelayer(bitcoinda.Config{ 12 | Host: "localhost:18332", 13 | User: "rpcuser", 14 | Pass: "rpcpass", 15 | HTTPPostMode: true, 16 | DisableTLS: true, 17 | }) 18 | if err != nil { 19 | fmt.Println(err) 20 | return 21 | } 22 | _, err = relayer.Write([]byte("rollkit-btc: gm")) 23 | if err != nil { 24 | fmt.Println(err) 25 | return 26 | } 27 | // TODO: either mock or generate block 28 | // We're assuming the prev tx was mined at height 146 29 | height := uint64(146) 30 | blobs, err := relayer.Read(height) 31 | if err != nil { 32 | fmt.Println(err) 33 | return 34 | } 35 | for _, blob := range blobs { 36 | got, err := hex.DecodeString(fmt.Sprintf("%x", blob)) 37 | if err != nil { 38 | fmt.Println(err) 39 | return 40 | } 41 | fmt.Println(string(got)) 42 | } 43 | // Output: rollkit-btc: gm 44 | } 45 | 46 | ## Tests 47 | 48 | Running the tests requires a local regtest node. 49 | 50 | bitcoind -chain=regtest -rpcport=18332 -rpcuser=rpcuser -rpcpassword=rpcpass -fallbackfee=0.000001 -txindex=1 51 | 52 | bitcoin-cli -regtest -rpcport=18332 -rpcuser=rpcuser -rpcpassword=rpcpass createwallet w1 53 | 54 | export COINBASE=$(bitcoin-cli -regtest -rpcport=18332 -rpcuser=rpcuser -rpcpassword=rpcpass getnewaddress) 55 | 56 | bitcoin-cli -regtest -rpcport=18332 -rpcuser=rpcuser -rpcpassword=rpcpass generatetoaddress 101 $COINBASE 57 | 58 | Idle for a while till coinbase coins mature. 59 | 60 | === RUN ExampleRelayer_Write 61 | --- PASS: ExampleRelayer_Write (0.13s) 62 | === RUN ExampleRelayer_Read 63 | --- PASS: ExampleRelayer_Read (0.10s) 64 | PASS 65 | ok github.com/rollkit/bitcoin-da 0.375s 66 | 67 | ## Writer 68 | 69 | A commit transaction containing a taproot with one leaf script 70 | 71 | OP_FALSE 72 | OP_IF 73 | "roll" marker 74 | 75 | OP_ENDIF 76 | 77 | OP_CHECKSIG 78 | 79 | is used to create a new bech32m address and is sent an output. 80 | 81 | A reveal transaction then posts the embedded data on chain and spends the 82 | commit output. 83 | 84 | ## Reader 85 | 86 | The address of the reveal transaction is implicity used as a namespace. 87 | 88 | Clients may call listunspent on the reveal transaction address to get a list of 89 | transactions and read the embedded data from the first witness input. 90 | 91 | ## Spec 92 | 93 | For more details, [read the spec](./spec.md) 94 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | # Bitcoin DA 2 | 3 | Overview on how to use Bitcoin for data availability. 4 | 5 | ## Goal 6 | 7 | Define a common read and write path that can be used by rollup development kits such as rollkit or op-stack that wish to use Bitcoin for DA. 8 | 9 | ## Background 10 | 11 | - [Ordinals docs](https://docs.ordinals.com/introduction.html) 12 | - [Why a Bitcoiner Loves Rollups | Eric Wall](https://www.youtube.com/watch?v=_hLvvZGST_E&t=924s) 13 | 14 | ## How? 15 | 16 | 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. 17 | 18 | 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. 19 | 20 | ## Write Path 21 | 22 | In order to inscribe our data into a satoshi, we need to use Taproot scripts, since our data is stored in taproot script-path spend scripts. Taproot script spends can only be made from existing taproot outputs, thus inscriptions are made with a two-phase commit/reveal scheme: 23 | 24 | 1. First, in the commit transaction, a taproot output committing to a script containing the inscription content is created. 25 | 2. Second, in the reveal transaction, the output created by the commit transaction is spent, revealing the inscription content on-chain. 26 | 27 | But what about the actual data: 28 | 29 | “Inscription content is serialized using data pushes within unexecuted conditionals, called an "envelopes". Envelopes consist of an `OP_FALSE OP_IF … OP_ENDIF` 30 |  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.” 31 | 32 | In the case of a rollup posting data into bitcoin, we could serialize the data as follows: 33 | 34 | ```nasm 35 | OP_FALSE 36 | OP_IF 37 | OP_PUSH "block" 38 | OP_1 39 | OP_PUSH $ROLLUP_BLOCK_HEIGHT 40 | OP_0 41 | OP_PUSH $BLOCK_DATA_PORTION 42 | ... 43 | OP_ENDIF 44 | ``` 45 | 46 | The above would create an inscription that first pushes `block`, in order to disambiguate rollup block inscriptions from other uses of envelopes. `OP_1` indicates that the next push contains the rollup height for which the data belongs too, and OP_0 indicates that any subsequent data push contains the data itself, since by Taproot restrictions, individual pushes may not be larger than 520 bytes, thus making it necessary to make multiple pushes for block data larger than 520 bytes. 47 | 48 | The inscription content is contained within the input of a reveal transaction, and the inscription is made on the first sat of its first output. 49 | 50 | ## Read Path 51 | 52 | Since every rollup using Bitcoin for DA will require a private key with some bitcoin to spend and create inscriptions with, we can always query all inscriptions by listing the rollup’s bitcoin address’ unspent outputs by using `[listunspent](https://developer.bitcoin.org/reference/rpc/listunspent.html)`. This will provide us with a list of utxos: 53 | 54 | ```nasm 55 | [ (json array) 56 | { (json object) 57 | "txid" : "hex", (string) the transaction id 58 | "vout" : n, (numeric) the vout value 59 | "address" : "str", (string) the bitcoin address 60 | "label" : "str", (string) The associated label, or "" for the default label 61 | "scriptPubKey" : "str", (string) the script key 62 | "amount" : n, (numeric) the transaction output amount in BTC 63 | "confirmations" : n, (numeric) The number of confirmations 64 | "redeemScript" : "hex", (string) The redeemScript if scriptPubKey is P2SH 65 | "witnessScript" : "str", (string) witnessScript if the scriptPubKey is P2WSH or P2SH-P2WSH 66 | "spendable" : true|false, (boolean) Whether we have the private keys to spend this output 67 | "solvable" : true|false, (boolean) Whether we know how to spend this output, ignoring the lack of keys 68 | "reused" : true|false, (boolean) (only present if avoid_reuse is set) Whether this output is reused/dirty (sent to an address that was previously spent from) 69 | "desc" : "str", (string) (only when solvable) A descriptor for spending this output 70 | "safe" : true|false (boolean) Whether this output is considered safe to spend. Unconfirmed transactions 71 | from outside keys and unconfirmed replacement transactions are considered unsafe 72 | and are not eligible for spending by fundrawtransaction and sendtoaddress. 73 | }, 74 | ... 75 | ] 76 | ``` 77 | 78 | We can then take this list of UTXOs and filter it to include only utxos that correspond to our reveal transactions, and additionally, we can order them by timestamp, thus giving us a list of utxo inscriptions for which we can associate an index to a certain block height in our rollup (i.e `utxo[0].witness == rollup_block_height_1_data`) 79 | 80 | Once we have found the utxo with the data for a given block, we can call `gettransaction` with the `tx_id` of our utxo, which will return: 81 | 82 | ```nasm 83 | { (json object) 84 | "amount" : n, (numeric) The amount in BTC 85 | "fee" : n, (numeric) The amount of the fee in BTC. This is negative and only available for the 86 | 'send' category of transactions. 87 | "confirmations" : n, (numeric) The number of confirmations for the transaction. Negative confirmations means the 88 | transaction conflicted that many blocks ago. 89 | "generated" : true|false, (boolean) Only present if transaction only input is a coinbase one. 90 | "trusted" : true|false, (boolean) Only present if we consider transaction to be trusted and so safe to spend from. 91 | "blockhash" : "hex", (string) The block hash containing the transaction. 92 | "blockheight" : n, (numeric) The block height containing the transaction. 93 | "blockindex" : n, (numeric) The index of the transaction in the block that includes it. 94 | "blocktime" : xxx, (numeric) The block time expressed in UNIX epoch time. 95 | "txid" : "hex", (string) The transaction id. 96 | "walletconflicts" : [ (json array) Conflicting transaction ids. 97 | "hex", (string) The transaction id. 98 | ... 99 | ], 100 | "time" : xxx, (numeric) The transaction time expressed in UNIX epoch time. 101 | "timereceived" : xxx, (numeric) The time received expressed in UNIX epoch time. 102 | "comment" : "str", (string) If a comment is associated with the transaction, only present if not empty. 103 | "bip125-replaceable" : "str", (string) ("yes|no|unknown") Whether this transaction could be replaced due to BIP125 (replace-by-fee); 104 | may be unknown for unconfirmed transactions not in the mempool 105 | "details" : [ (json array) 106 | { (json object) 107 | "involvesWatchonly" : true|false, (boolean) Only returns true if imported addresses were involved in transaction. 108 | "address" : "str", (string) The bitcoin address involved in the transaction. 109 | "category" : "str", (string) The transaction category. 110 | "send" Transactions sent. 111 | "receive" Non-coinbase transactions received. 112 | "generate" Coinbase transactions received with more than 100 confirmations. 113 | "immature" Coinbase transactions received with 100 or fewer confirmations. 114 | "orphan" Orphaned coinbase transactions received. 115 | "amount" : n, (numeric) The amount in BTC 116 | "label" : "str", (string) A comment for the address/transaction, if any 117 | "vout" : n, (numeric) the vout value 118 | "fee" : n, (numeric) The amount of the fee in BTC. This is negative and only available for the 119 | 'send' category of transactions. 120 | "abandoned" : true|false (boolean) 'true' if the transaction has been abandoned (inputs are respendable). Only available for the 121 | 'send' category of transactions. 122 | }, 123 | ... 124 | ], 125 | "hex" : "hex", (string) Raw data for transaction 126 | "decoded" : { (json object) Optional, the decoded transaction (only present when `verbose` is passed) 127 | ... Equivalent to the RPC decoderawtransaction method, or the RPC getrawtransaction method when `verbose` is passed. 128 | } 129 | } 130 | ``` 131 | 132 | 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. 133 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 2 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 3 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 4 | github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= 5 | github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= 6 | github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= 7 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 8 | github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= 9 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= 10 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 11 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 12 | github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= 13 | github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= 14 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 15 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= 16 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 17 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= 18 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 19 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 20 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= 21 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 22 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 23 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 24 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 25 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 26 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= 27 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 28 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 29 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 32 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 34 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 35 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 36 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 37 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 38 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 39 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 40 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 42 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 43 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 44 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 45 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 46 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 47 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 48 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 49 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 50 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 51 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 52 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 53 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 54 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 55 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 56 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 57 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 58 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 59 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 60 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 61 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 62 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 63 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 64 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/rollkit/go-da v0.5.0 h1:sQpZricNS+2TLx3HMjNWhtRfqtvVC/U4pWHpfUz3eN4= 68 | github.com/rollkit/go-da v0.5.0/go.mod h1:VsUeAoPvKl4Y8wWguu/VibscYiFFePkkrvZWyTjZHww= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 72 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 73 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 74 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 75 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 76 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 77 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 78 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 79 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 80 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 81 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 82 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 83 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 84 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 85 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 86 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 87 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 95 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 96 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 97 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 98 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 99 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 100 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 102 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 103 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 104 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 105 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 106 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 107 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 108 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 109 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 110 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 111 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 112 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 113 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 114 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 115 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 116 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /relayer.go: -------------------------------------------------------------------------------- 1 | package bitcoinda 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/btcsuite/btcd/btcec/v2" 8 | "github.com/btcsuite/btcd/btcec/v2/schnorr" 9 | "github.com/btcsuite/btcd/btcutil" 10 | "github.com/btcsuite/btcd/chaincfg" 11 | "github.com/btcsuite/btcd/chaincfg/chainhash" 12 | "github.com/btcsuite/btcd/rpcclient" 13 | "github.com/btcsuite/btcd/txscript" 14 | "github.com/btcsuite/btcd/wire" 15 | ) 16 | 17 | // PROTOCOL_ID allows data identification by looking at the first few bytes 18 | var PROTOCOL_ID = []byte{0x72, 0x6f, 0x6c, 0x6c} 19 | 20 | // Sample data and keys for testing. 21 | // bob key pair is used for signing reveal tx 22 | // internal key pair is used for tweaking 23 | var ( 24 | bobPrivateKey = "5JoQtsKQuH8hC9MyvfJAqo6qmKLm8ePYNucs7tPu2YxG12trzBt" 25 | internalPrivateKey = "5JGgKfRy6vEcWBpLJV5FXUfMGNXzvdWzQHUM1rVLEUJfvZUSwvS" 26 | ) 27 | 28 | // chunkSlice splits input slice into max chunkSize length slices 29 | func chunkSlice(slice []byte, chunkSize int) [][]byte { 30 | var chunks [][]byte 31 | for i := 0; i < len(slice); i += chunkSize { 32 | end := i + chunkSize 33 | 34 | // necessary check to avoid slicing beyond 35 | // slice capacity 36 | if end > len(slice) { 37 | end = len(slice) 38 | } 39 | 40 | chunks = append(chunks, slice[i:end]) 41 | } 42 | 43 | return chunks 44 | } 45 | 46 | // createTaprootAddress returns an address committing to a Taproot script with 47 | // a single leaf containing the spend path with the script: 48 | // OP_DROP OP_CHECKSIG 49 | func createTaprootAddress(embeddedData []byte) (string, error) { 50 | privKey, err := btcutil.DecodeWIF(bobPrivateKey) 51 | if err != nil { 52 | return "", fmt.Errorf("error decoding bob private key: %w", err) 53 | } 54 | 55 | pubKey := privKey.PrivKey.PubKey() 56 | 57 | // Step 1: Construct the Taproot script with one leaf. 58 | builder := txscript.NewScriptBuilder() 59 | builder.AddOp(txscript.OP_0) 60 | builder.AddOp(txscript.OP_IF) 61 | chunks := chunkSlice(embeddedData, 520) 62 | for _, chunk := range chunks { 63 | builder.AddData(chunk) 64 | } 65 | builder.AddOp(txscript.OP_ENDIF) 66 | builder.AddData(schnorr.SerializePubKey(pubKey)) 67 | builder.AddOp(txscript.OP_CHECKSIG) 68 | pkScript, err := builder.Script() 69 | if err != nil { 70 | return "", fmt.Errorf("error building script: %w", err) 71 | } 72 | 73 | tapLeaf := txscript.NewBaseTapLeaf(pkScript) 74 | tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) 75 | 76 | internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey) 77 | if err != nil { 78 | return "", fmt.Errorf("error decoding internal private key: %w", err) 79 | } 80 | 81 | internalPubKey := internalPrivKey.PrivKey.PubKey() 82 | 83 | // Step 2: Generate the Taproot tree. 84 | tapScriptRootHash := tapScriptTree.RootNode.TapHash() 85 | outputKey := txscript.ComputeTaprootOutputKey( 86 | internalPubKey, tapScriptRootHash[:], 87 | ) 88 | 89 | // Step 3: Generate the Bech32m address. 90 | address, err := btcutil.NewAddressTaproot( 91 | schnorr.SerializePubKey(outputKey), &chaincfg.RegressionNetParams) 92 | if err != nil { 93 | return "", fmt.Errorf("error encoding Taproot address: %w", err) 94 | } 95 | 96 | return address.String(), nil 97 | } 98 | 99 | // payToTaprootScript creates a pk script for a pay-to-taproot output key. 100 | func payToTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) { 101 | return txscript.NewScriptBuilder(). 102 | AddOp(txscript.OP_1). 103 | AddData(schnorr.SerializePubKey(taprootKey)). 104 | Script() 105 | } 106 | 107 | // Relayer is a bitcoin client wrapper which provides reader and writer methods 108 | // to write binary blobs to the blockchain. 109 | type Relayer struct { 110 | client *rpcclient.Client 111 | } 112 | 113 | // close shuts down the client. 114 | func (r Relayer) close() { // nolint:unused 115 | r.client.Shutdown() 116 | } 117 | 118 | // commitTx commits an output to the given taproot address, such that the 119 | // output is only spendable by posting the embedded data on chain, as part of 120 | // the script satisfying the tapscript spend path that commits to the data. It 121 | // returns the hash of the commit transaction and error, if any. 122 | func (r Relayer) commitTx(addr string) (*chainhash.Hash, error) { 123 | // Create a transaction that sends 0.001 BTC to the given address. 124 | address, err := btcutil.DecodeAddress(addr, &chaincfg.RegressionNetParams) 125 | if err != nil { 126 | return nil, fmt.Errorf("error decoding recipient address: %w", err) 127 | } 128 | 129 | amount, err := btcutil.NewAmount(0.001) 130 | if err != nil { 131 | return nil, fmt.Errorf("error creating new amount: %w", err) 132 | } 133 | 134 | hash, err := r.client.SendToAddress(address, amount) 135 | if err != nil { 136 | return nil, fmt.Errorf("error sending to address: %w", err) 137 | } 138 | 139 | return hash, nil 140 | } 141 | 142 | // revealTx spends the output from the commit transaction and as part of the 143 | // script satisfying the tapscript spend path, posts the embedded data on 144 | // chain. It returns the hash of the reveal transaction and error, if any. 145 | func (r Relayer) revealTx(embeddedData []byte, commitHash *chainhash.Hash) (*chainhash.Hash, error) { 146 | rawCommitTx, err := r.client.GetRawTransaction(commitHash) 147 | if err != nil { 148 | return nil, fmt.Errorf("error getting raw commit tx: %w", err) 149 | } 150 | 151 | // TODO: use a better way to find our output 152 | var commitIndex int 153 | var commitOutput *wire.TxOut 154 | for i, out := range rawCommitTx.MsgTx().TxOut { 155 | if out.Value == 100000 { 156 | commitIndex = i 157 | commitOutput = out 158 | break 159 | } 160 | } 161 | 162 | privKey, err := btcutil.DecodeWIF(bobPrivateKey) 163 | if err != nil { 164 | return nil, fmt.Errorf("error decoding bob private key: %w", err) 165 | } 166 | 167 | pubKey := privKey.PrivKey.PubKey() 168 | 169 | internalPrivKey, err := btcutil.DecodeWIF(internalPrivateKey) 170 | if err != nil { 171 | return nil, fmt.Errorf("error decoding internal private key: %w", err) 172 | } 173 | 174 | internalPubKey := internalPrivKey.PrivKey.PubKey() 175 | 176 | // Our script will be a simple OP_DROP OP_CHECKSIG as the 177 | // sole leaf of a tapscript tree. 178 | builder := txscript.NewScriptBuilder() 179 | builder.AddOp(txscript.OP_0) 180 | builder.AddOp(txscript.OP_IF) 181 | chunks := chunkSlice(embeddedData, 520) 182 | for _, chunk := range chunks { 183 | builder.AddData(chunk) 184 | } 185 | builder.AddOp(txscript.OP_ENDIF) 186 | builder.AddData(schnorr.SerializePubKey(pubKey)) 187 | builder.AddOp(txscript.OP_CHECKSIG) 188 | pkScript, err := builder.Script() 189 | if err != nil { 190 | return nil, fmt.Errorf("error building script: %w", err) 191 | } 192 | 193 | tapLeaf := txscript.NewBaseTapLeaf(pkScript) 194 | tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf) 195 | 196 | ctrlBlock := tapScriptTree.LeafMerkleProofs[0].ToControlBlock( 197 | internalPubKey, 198 | ) 199 | 200 | tapScriptRootHash := tapScriptTree.RootNode.TapHash() 201 | outputKey := txscript.ComputeTaprootOutputKey( 202 | internalPubKey, tapScriptRootHash[:], 203 | ) 204 | p2trScript, err := payToTaprootScript(outputKey) 205 | if err != nil { 206 | return nil, fmt.Errorf("error building p2tr script: %w", err) 207 | } 208 | 209 | tx := wire.NewMsgTx(2) 210 | tx.AddTxIn(&wire.TxIn{ 211 | PreviousOutPoint: wire.OutPoint{ 212 | Hash: *rawCommitTx.Hash(), 213 | Index: uint32(commitIndex), // nolint:gosec 214 | }, 215 | }) 216 | txOut := &wire.TxOut{ 217 | Value: 1e3, PkScript: p2trScript, 218 | } 219 | tx.AddTxOut(txOut) 220 | 221 | inputFetcher := txscript.NewCannedPrevOutputFetcher( 222 | commitOutput.PkScript, 223 | commitOutput.Value, 224 | ) 225 | sigHashes := txscript.NewTxSigHashes(tx, inputFetcher) 226 | 227 | sig, err := txscript.RawTxInTapscriptSignature( 228 | tx, sigHashes, 0, txOut.Value, 229 | txOut.PkScript, tapLeaf, txscript.SigHashDefault, 230 | privKey.PrivKey, 231 | ) 232 | 233 | if err != nil { 234 | return nil, fmt.Errorf("error signing tapscript: %w", err) 235 | } 236 | 237 | // Now that we have the sig, we'll make a valid witness 238 | // including the control block. 239 | ctrlBlockBytes, err := ctrlBlock.ToBytes() 240 | if err != nil { 241 | return nil, fmt.Errorf("error including control block: %w", err) 242 | } 243 | tx.TxIn[0].Witness = wire.TxWitness{ 244 | sig, pkScript, ctrlBlockBytes, 245 | } 246 | 247 | hash, err := r.client.SendRawTransaction(tx, false) 248 | if err != nil { 249 | return nil, fmt.Errorf("error sending reveal transaction: %w", err) 250 | } 251 | return hash, nil 252 | } 253 | 254 | // Config is the relayer config 255 | type Config struct { 256 | Host string 257 | User string 258 | Pass string 259 | HTTPPostMode bool 260 | DisableTLS bool 261 | } 262 | 263 | // NewRelayer returns a new relayer. It can error if there's an RPC connection 264 | // error with the connection config. 265 | func NewRelayer(config Config) (*Relayer, error) { 266 | // Set up the connection to the btcd RPC server. 267 | // NOTE: for testing bitcoind can be used in regtest with the following params - 268 | // bitcoind -chain=regtest -rpcport=18332 -rpcuser=rpcuser -rpcpassword=rpcpass -fallbackfee=0.000001 -txindex=1 269 | connCfg := &rpcclient.ConnConfig{ 270 | Host: config.Host, 271 | User: config.User, 272 | Pass: config.Pass, 273 | HTTPPostMode: config.HTTPPostMode, 274 | DisableTLS: config.DisableTLS, 275 | } 276 | client, err := rpcclient.New(connCfg, nil) 277 | if err != nil { 278 | return nil, fmt.Errorf("error creating btcd RPC client: %w", err) 279 | } 280 | return &Relayer{ 281 | client: client, 282 | }, nil 283 | } 284 | 285 | // ReadTransaction reads a transaction from the blockchain given a hash. 286 | func (r Relayer) ReadTransaction(hash *chainhash.Hash) ([]byte, error) { 287 | tx, err := r.client.GetRawTransaction(hash) 288 | if err != nil { 289 | return nil, err 290 | } 291 | if len(tx.MsgTx().TxIn[0].Witness) > 1 { 292 | witness := tx.MsgTx().TxIn[0].Witness[1] 293 | pushData, err := ExtractPushData(0, witness) 294 | if err != nil { 295 | return nil, err 296 | } 297 | // skip PROTOCOL_ID 298 | if pushData != nil && bytes.HasPrefix(pushData, PROTOCOL_ID) { 299 | return pushData[4:], nil 300 | } 301 | } 302 | return nil, nil 303 | } 304 | 305 | func (r Relayer) Read(height uint64) ([][]byte, error) { 306 | hash, err := r.client.GetBlockHash(int64(height)) // nolint:gosec 307 | if err != nil { 308 | return nil, err 309 | } 310 | block, err := r.client.GetBlock(hash) 311 | if err != nil { 312 | return nil, err 313 | } 314 | 315 | var data [][]byte 316 | for _, tx := range block.Transactions { 317 | if len(tx.TxIn[0].Witness) > 1 { 318 | witness := tx.TxIn[0].Witness[1] 319 | pushData, err := ExtractPushData(0, witness) 320 | if err != nil { 321 | return nil, err 322 | } 323 | // skip PROTOCOL_ID 324 | if pushData != nil && bytes.HasPrefix(pushData, PROTOCOL_ID) { 325 | data = append(data, pushData[4:]) 326 | } 327 | } 328 | } 329 | return data, nil 330 | } 331 | 332 | func (r Relayer) Write(data []byte) (*chainhash.Hash, error) { 333 | data = append(PROTOCOL_ID, data...) 334 | address, err := createTaprootAddress(data) 335 | if err != nil { 336 | return nil, err 337 | } 338 | hash, err := r.commitTx(address) 339 | if err != nil { 340 | return nil, err 341 | } 342 | hash, err = r.revealTx(data, hash) 343 | if err != nil { 344 | return nil, err 345 | } 346 | return hash, nil 347 | } 348 | 349 | // ExtractPushData ... 350 | func ExtractPushData(version uint16, pkScript []byte) ([]byte, error) { 351 | type templateMatch struct { 352 | expectPushData bool 353 | maxPushDatas int 354 | opcode byte 355 | extractedData []byte 356 | } 357 | var template = [6]templateMatch{ 358 | {opcode: txscript.OP_FALSE}, 359 | {opcode: txscript.OP_IF}, 360 | {expectPushData: true, maxPushDatas: 10}, 361 | {opcode: txscript.OP_ENDIF}, 362 | {expectPushData: true, maxPushDatas: 1}, 363 | {opcode: txscript.OP_CHECKSIG}, 364 | } 365 | 366 | var templateOffset int 367 | tokenizer := txscript.MakeScriptTokenizer(version, pkScript) 368 | out: 369 | for tokenizer.Next() { 370 | // Not a rollkit script if it has more opcodes than expected in the 371 | // template. 372 | if templateOffset >= len(template) { 373 | return nil, nil 374 | } 375 | 376 | op := tokenizer.Opcode() 377 | tplEntry := &template[templateOffset] 378 | if tplEntry.expectPushData { 379 | for i := 0; i < tplEntry.maxPushDatas; i++ { 380 | data := tokenizer.Data() 381 | if data == nil { 382 | break out 383 | } 384 | tplEntry.extractedData = append(tplEntry.extractedData, data...) 385 | tokenizer.Next() 386 | } 387 | } else if op != tplEntry.opcode { 388 | return nil, nil 389 | } 390 | 391 | templateOffset++ 392 | } 393 | // TODO: skipping err checks 394 | return template[2].extractedData, nil 395 | } 396 | --------------------------------------------------------------------------------