Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
106 changes: 57 additions & 49 deletions pkg/template/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,90 +29,98 @@ 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
}
pk, err := btcec.ParsePubKey(v.raw)
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
Expand Down
15 changes: 15 additions & 0 deletions pkg/template/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":["<owner>","OP_DROP","OP_1"]},"leaves":[{"name":"pay","asm":["<SERVER_KEY>","OP_CHECKSIGVERIFY","<EMULATOR_KEY:pay>","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":[
Expand Down
Loading