Skip to content

Commit

Permalink
Merge pull request #214 from doitian/docs/custom-script
Browse files Browse the repository at this point in the history
docs: add docs and examples for signer and builder
  • Loading branch information
quake authored Aug 9, 2024
2 parents b4f527f + 89c1d39 commit f9c3a48
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 5 deletions.
9 changes: 7 additions & 2 deletions collector/builder/builder.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// Package builder implements CKB transaction builder.
package builder

import (
"errors"
"reflect"
"strconv"

"github.com/nervosnetwork/ckb-sdk-go/v2/address"
"github.com/nervosnetwork/ckb-sdk-go/v2/collector"
"github.com/nervosnetwork/ckb-sdk-go/v2/collector/handler"
"github.com/nervosnetwork/ckb-sdk-go/v2/transaction"
"github.com/nervosnetwork/ckb-sdk-go/v2/types"
"reflect"
"strconv"
)

type SimpleTransactionBuilder struct {
Expand All @@ -24,6 +26,9 @@ type SimpleTransactionBuilder struct {
ScriptHandlers []collector.ScriptHandler
}

// NewSimpleTransactionBuilder creates a new transaction builder and register the [collector.ScriptHandler] of popular scripts for the specific network.
//
// To create an empty builder without script handlers, just uses '&SimpleTransactionBuilder{}'.
func NewSimpleTransactionBuilder(network types.Network) *SimpleTransactionBuilder {
if network == types.NetworkMain || network == types.NetworkTest {
s := SimpleTransactionBuilder{}
Expand Down
57 changes: 57 additions & 0 deletions collector/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package collector_test

import (
"reflect"

"github.com/nervosnetwork/ckb-sdk-go/v2/collector"
"github.com/nervosnetwork/ckb-sdk-go/v2/collector/builder"
"github.com/nervosnetwork/ckb-sdk-go/v2/transaction"
"github.com/nervosnetwork/ckb-sdk-go/v2/types"
)

// SimpleLockScriptHandler is an example script handler to add specified cell dep and prefill the witness.
type SimpleLockScriptHandler struct {
CellDep *types.CellDep
WitnessPlaceholder []byte
CodeHash types.Hash
}

func (r *SimpleLockScriptHandler) isMatched(script *types.Script) bool {
if script == nil {
return false
}
return reflect.DeepEqual(script.CodeHash, r.CodeHash)
}

func (r *SimpleLockScriptHandler) BuildTransaction(builder collector.TransactionBuilder, group *transaction.ScriptGroup, context interface{}) (bool, error) {
// Only run on matched groups
if group == nil || !r.isMatched(group.Script) {
return false, nil
}
index := group.InputIndices[0]
// set the witness placeholder
if err := builder.SetWitness(uint(index), types.WitnessTypeLock, r.WitnessPlaceholder); err != nil {
return false, err
}
// CkbTransactionBuilder.AddCellDep will remove duplications automatically.
builder.AddCellDep(r.CellDep)
return true, nil
}

func ExampleScriptHandler() {
txHash := "0x1234"
typeScriptHash := "0xabcd"

s := builder.SimpleTransactionBuilder{}
s.Register(&SimpleLockScriptHandler{
CellDep: &types.CellDep{
OutPoint: &types.OutPoint{
TxHash: types.HexToHash(txHash),
Index: 0,
},
DepType: types.DepTypeCode,
},
CodeHash: types.HexToHash(typeScriptHash),
WitnessPlaceholder: make([]byte, 8),
})
}
14 changes: 14 additions & 0 deletions collector/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ type TransactionBuilder interface {
Build(contexts ...interface{}) (*transaction.TransactionWithScriptGroups, error)
}

// The interface ScriptHandler is for scripts to register their building logic.
//
// The function BuildTransaction is the callback called by [TransactionBuilder]
// for each script group and each context passed in
// TransactionBuilder.Build. The context provides extra data for the script.
//
// Be calfully on when to run the logic for the script. TransactionBuilder will
// not check whether the script group matches the script.
//
// The callback often does two things:
// - Fill witness placeholder to make fee calculation correct.
// - Add cell deps for the script.
//
// Returns bool indicating whether the transaction has been modified.
type ScriptHandler interface {
BuildTransaction(builder TransactionBuilder, group *transaction.ScriptGroup, context interface{}) (bool, error)
}
7 changes: 6 additions & 1 deletion transaction/context.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package transaction

import (
"github.com/nervosnetwork/ckb-sdk-go/v2/crypto/secp256k1"
"github.com/pkg/errors"

"github.com/nervosnetwork/ckb-sdk-go/v2/crypto/secp256k1"
)

// Context is user provided information for
// `signer.TransactionSigner.SignTransaction`. In turn the context is passed to `signer.ScriptSigner.SignTransaction`.
//
// See more in github.com/nervosnetwork/ckb-sdk-go/v2/transaction/signer
type Context struct {
Key *secp256k1.Secp256k1Key
Payload interface{}
Expand Down
84 changes: 84 additions & 0 deletions transaction/signer/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package signer_test

import (
"context"
"encoding/binary"
"reflect"

"github.com/nervosnetwork/ckb-sdk-go/v2/rpc"
"github.com/nervosnetwork/ckb-sdk-go/v2/transaction"
"github.com/nervosnetwork/ckb-sdk-go/v2/transaction/signer"
"github.com/nervosnetwork/ckb-sdk-go/v2/types"
)

type CapacityDiffContext struct {
rpc rpc.Client
ctx context.Context
}

func (ctx CapacityDiffContext) getInputCell(outPoint *types.OutPoint) (*types.CellOutput, error) {
cellWithStatus, err := ctx.rpc.GetLiveCell(ctx.ctx, outPoint, false)
if err != nil {
return nil, err
}

return cellWithStatus.Cell.Output, nil
}

type CapacityDiffScriptSigner struct{}

func (s *CapacityDiffScriptSigner) SignTransaction(tx *types.Transaction, group *transaction.ScriptGroup, ctx *transaction.Context) (bool, error) {
scriptContext, ok := ctx.Payload.(CapacityDiffContext)
if !ok {
return false, nil
}

total := int64(0)
for _, i := range group.InputIndices {
inputCell, err := scriptContext.getInputCell(tx.Inputs[i].PreviousOutput)
if err != nil {
return false, nil
}
total -= int64(inputCell.Capacity)
}
for _, output := range tx.Outputs {
if reflect.DeepEqual(output.Lock, group.Script) {
total += int64(output.Capacity)
}
}

// The specification https://go.dev/ref/spec#Numeric_types says integres in
// Go are repsented using two's complementation. So we can just cast it to
// uin64 and get the little endian bytes.
witness := make([]byte, 8)
binary.LittleEndian.PutUint64(witness, uint64(total))

witnessIndex := group.InputIndices[0]
witnessArgs, err := types.DeserializeWitnessArgs(tx.Witnesses[witnessIndex])
if err != nil {
return false, err
}
witnessArgs.Lock = witness
tx.Witnesses[witnessIndex] = witnessArgs.Serialize()

return true, nil
}

// This example demonstrates how to use a custom script CapacityDiff
// (https://github.com/doitian/ckb-sdk-examples-capacity-diff).
//
// CapacityDiff verifies the witness matches the capacity difference.
//
// - The script loads the witness for the first input in the script group using the WitnessArgs layout.
// - The total input capacity is the sum of all the input cells in the script group.
// - The total output capacity is the sum of all the output cells having the same lock script as the script group.
// - The capacity difference is a 64-bit signed integer which equals to total output capacity minus total input capacity.
// - The witness is encoded using two's complement and little endian.
func ExampleScriptSigner() {
signer := signer.NewTransactionSigner()
signer.RegisterSigner(
types.HexToHash("0x6283a479a3cf5d4276cd93594de9f1827ab9b55c7b05b3d28e4c2e0a696cfefd"),
types.ScriptTypeType,
&CapacityDiffScriptSigner{},
)
}
23 changes: 21 additions & 2 deletions transaction/signer/signer.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
// Package signer implements a CKB transaction signing framework.
//
// It adopts an extension mechanism that new script can implement
// [ScriptSigner] and register the signing logic via
// [TransactionSigner.RegisterSigner].
package signer

import (
"fmt"

"github.com/nervosnetwork/ckb-sdk-go/v2/crypto/blake2b"
"github.com/nervosnetwork/ckb-sdk-go/v2/transaction"
"github.com/nervosnetwork/ckb-sdk-go/v2/types"
)

// The interface ScriptSigner is for scripts to register their signing logic.
//
// The function SignTransaction is the callback called by [TransactionSigner]
// on matched ScriptSigners, for each context passed in
// [TransactionSigner.SignTransaction].
//
// The [transaction.Context] provides extra data for the signer. For example,
// [Secp256k1Blake160SighashAllSigner.SignTransaction] requires user to pass
// the private keys as contexts.
//
// Returns bool indicating whether the transaction has been modified.
type ScriptSigner interface {
SignTransaction(transaction *types.Transaction, group *transaction.ScriptGroup, ctx *transaction.Context) (bool, error)
}
Expand All @@ -19,8 +36,10 @@ func NewTransactionSigner() *TransactionSigner {
return &TransactionSigner{signers: make(map[types.Hash]ScriptSigner)}
}

var testInstance = NewTransactionSigner()
var mainInstance = NewTransactionSigner()
var (
testInstance = NewTransactionSigner()
mainInstance = NewTransactionSigner()
)

func GetTransactionSignerInstance(network types.Network) *TransactionSigner {
if network == types.NetworkTest {
Expand Down
2 changes: 2 additions & 0 deletions transaction/transaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package transaction implements CKB transaction signing.
package transaction

0 comments on commit f9c3a48

Please sign in to comment.