diff --git a/Makefile b/Makefile index 4d40a47..67c72da 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) BINARY = delegateed LDFLAGS := -s -w -X 'main.Version=$(VERSION)' -.PHONY: build build-all clean test test-e2e lint fmt deps docker regtest-up regtest-run regtest-down run proto proto-lint pgsqlc pgmigrate +.PHONY: build build-all clean test test-e2e lint lint-fix fmt deps docker regtest-up regtest-run regtest-down run proto proto-lint pgsqlc pgmigrate build: go build -ldflags="$(LDFLAGS)" -o bin/$(BINARY) ./cmd/delegateed @@ -30,6 +30,10 @@ lint: golangci-lint run ./... cd pkg/template && golangci-lint run ./... +lint-fix: + golangci-lint run --fix ./... + cd pkg/template && golangci-lint run --fix ./... + ## proto: regenerates api-spec/protobuf/gen and the openapi spec with buf (docker) proto: proto-lint @docker run --rm --volume "$(shell pwd):/workspace" --workdir /workspace buf generate diff --git a/pkg/template/contract.go b/pkg/template/contract.go index e969dc6..97b8624 100644 --- a/pkg/template/contract.go +++ b/pkg/template/contract.go @@ -29,69 +29,88 @@ func build(def *Definition, args map[string]value, keys Keys) (*contract, error) return buildTree(def, args, keys, nil) } -// buildTree checks the declared leaves against taptree when it is given, and takes the tree's leaves and key. -func buildTree(def *Definition, args map[string]value, keys Keys, taptree []byte) (*contract, error) { - field := func(name string) (value, error) { - v, ok := args[name] - if !ok { - return value{}, fmt.Errorf("unknown constructor value %q", name) - } - return v, nil +func field(args map[string]value, name string) (value, error) { + v, ok := args[name] + if !ok { + return value{}, fmt.Errorf("unknown constructor value %q", name) } + return v, nil +} - delegate := func() (*btcec.PublicKey, error) { - if keys.Delegate == nil { - return nil, fmt.Errorf("%w: no delegate key", ErrUnsupported) - } - return keys.Delegate, nil +func keyField(args map[string]value, name string) (value, error) { + v, err := field(args, name) + if err != nil || v.typ != "pubkey" { + return v, err } - // intent messages spell cosigner keys in hex - covenantLookup := func(name string) (value, error) { - if name != "DELEGATE_KEY" { - return field(name) - } - pk, err := delegate() - if err != nil { - return value{}, err - } - return value{"bytes", []byte(hex.EncodeToString(pk.SerializeCompressed()))}, nil + pk, err := btcec.ParsePubKey(v.raw) + if err != nil { + return value{}, err + } + return value{"bytes32", schnorr.SerializePubKey(pk)}, nil +} + +func delegate(keys Keys) (*btcec.PublicKey, error) { + if keys.Delegate == nil { + return nil, fmt.Errorf("%w: no delegate key", ErrUnsupported) + } + return keys.Delegate, nil +} + +func covenantLookup(keys Keys, args map[string]value, name string) (value, error) { + if name != "DELEGATE_KEY" { + return keyField(args, name) + } + pk, err := delegate(keys) + if err != nil { + return value{}, err + } + return value{"bytes", []byte(hex.EncodeToString(pk.SerializeCompressed()))}, nil +} + +func tweak(c *contract, key *btcec.PublicKey, fn string) (value, error) { + cov, ok := c.covenants[fn] + if !ok { + return value{}, fmt.Errorf("function %q has no covenant", fn) + } + t := arkade.ComputeArkadeScriptPublicKey(key, arkade.ArkadeScriptHash(cov)) + return value{"bytes32", schnorr.SerializePubKey(t)}, nil +} + +// buildTree checks the declared leaves against taptree when it is given, and takes the tree's leaves and key. +func buildTree(def *Definition, args map[string]value, keys Keys, taptree []byte) (*contract, error) { + c := &contract{ + covenants: map[string][]byte{}, + proofs: map[[2]string]*psbt.TaprootTapLeafScript{}, } - c := &contract{covenants: map[string][]byte{}, proofs: map[[2]string]*psbt.TaprootTapLeafScript{}} for _, fn := range def.Functions { if fn.Arkade == nil { continue } - s, err := assemble(fn.Arkade.Asm, covenantLookup) + s, err := assemble(fn.Arkade.Asm, func(name string) (value, error) { + return covenantLookup(keys, args, name) + }) if err != nil { return nil, fmt.Errorf("%w: function %q: %v", ErrInvalidTemplate, fn.Name, err) } c.covenants[fn.Name] = s } - tweak := func(key *btcec.PublicKey, fn string) (value, error) { - cov, ok := c.covenants[fn] - if !ok { - return value{}, fmt.Errorf("function %q has no covenant", fn) - } - t := arkade.ComputeArkadeScriptPublicKey(key, arkade.ArkadeScriptHash(cov)) - return value{"bytes32", schnorr.SerializePubKey(t)}, nil - } leafLookup := func(name string) (value, error) { parts := strings.Split(name, ":") switch { case name == "SERVER_KEY": return value{"bytes32", schnorr.SerializePubKey(keys.Server)}, nil case name == "DELEGATE_KEY": - pk, err := delegate() + pk, err := delegate(keys) if err != nil { return value{}, err } return value{"bytes32", schnorr.SerializePubKey(pk)}, nil case len(parts) == 2 && parts[0] == "EMULATOR_KEY": - return tweak(keys.Emulator, parts[1]) + return tweak(c, keys.Emulator, parts[1]) case len(parts) == 3 && parts[0] == "TWEAK": - v, err := field(parts[1]) + v, err := field(args, parts[1]) if err != nil { return value{}, err } @@ -99,20 +118,9 @@ func buildTree(def *Definition, args map[string]value, keys Keys, taptree []byte if err != nil { return value{}, err } - return tweak(pk, parts[2]) - } - v, err := field(name) - if err != nil { - return value{}, err - } - if v.typ == "pubkey" { - pk, err := btcec.ParsePubKey(v.raw) - if err != nil { - return value{}, err - } - return value{"bytes32", schnorr.SerializePubKey(pk)}, nil + return tweak(c, pk, parts[2]) } - return v, nil + return keyField(args, name) } var leaves []txscript.TapLeaf diff --git a/pkg/template/contract_test.go b/pkg/template/contract_test.go index bb69d53..a8e9400 100644 --- a/pkg/template/contract_test.go +++ b/pkg/template/contract_test.go @@ -63,6 +63,21 @@ func TestBuildTreeShapes(t *testing.T) { } } +// A covenant compares a key against a taproot witness program, so it needs the +// x-only bytes a leaf gets; arkadec's own e2e binds pubkeys the same way. +func TestBuildCovenantPushesXOnlyPubkey(t *testing.T) { + owner := testKey(t, 2) + def := parseDef(t, `{"contractName":"C","constructorInputs":[{"name":"owner","type":"pubkey"}],"functions":[ + {"name":"pay","arkade":{"inputs":[],"asm":["","OP_DROP","OP_1"]},"leaves":[{"name":"pay","asm":["","OP_CHECKSIGVERIFY","","OP_CHECKSIG"],"witness":[]}]}]}`) + c, err := build(def, map[string]value{"owner": {"pubkey", owner.SerializeCompressed()}}, Keys{Server: testKey(t, 1), Emulator: testKey(t, 3)}) + require.NoError(t, err) + + want, err := txscript.NewScriptBuilder(). + AddData(schnorr.SerializePubKey(owner)).AddOp(txscript.OP_DROP).AddOp(txscript.OP_1).Script() + require.NoError(t, err) + require.Equal(t, want, c.covenants["pay"]) +} + func TestBuildRejects(t *testing.T) { keys := Keys{Server: testKey(t, 1), Emulator: testKey(t, 3)} def := parseDef(t, `{"contractName":"C","constructorInputs":[],"functions":[