Skip to content

Commit

Permalink
feat(ipld): simplify ipldutils API
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Jun 10, 2022
1 parent 7bddd07 commit 7765cd2
Show file tree
Hide file tree
Showing 23 changed files with 496 additions and 526 deletions.
105 changes: 105 additions & 0 deletions bindnodeutils/bindnodeutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package bindnodeutils

import (
"fmt"
"io"
"reflect"

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/schema"
)

// We use the prototype map to store TypedPrototype and Type information
// and bindnode options mapped against the Go type so we only have to run the
// schema parse once.
type prototypeData struct {
proto schema.TypedPrototype
options []bindnode.Option
}

var prototype map[reflect.Type]prototypeData = make(map[reflect.Type]prototypeData)

func typeOf(ptrValue interface{}) reflect.Type {
val := reflect.ValueOf(ptrValue).Type()
for val.Kind() == reflect.Ptr {
val = val.Elem()
}
return val
}

// lookup of cached TypedPrototype (and therefore Type) for a Go type, if not
// found, initial parse and setup and caching of the TypedPrototype will happen
func prototypeDataFor(ptrType interface{}) prototypeData {
typ := typeOf(ptrType)
proto, ok := prototype[typ]
if !ok {
panic(fmt.Sprintf("bindnode utils: type has not been registered: %s", typ.Name()))
}
return proto
}

// RegisterType registers ptrType with schema such that it can be wrapped and
// unwrapped without needing the schema, Type, or TypedPrototype.
// Typically the typeName will match the Go type name, but it can be whatever
// is defined in the schema for the type being registered.
//
// May panic if the schema is invalid or the type doesn't match the schema.
func RegisterType(ptrType interface{}, schema string, typeName string, options ...bindnode.Option) {
typ := typeOf(ptrType)
typeSystem, err := ipld.LoadSchemaBytes([]byte(schema))
if err != nil {
panic(fmt.Sprintf("bindnode utils: failed to load schema: %s", err.Error()))
}
schemaType := typeSystem.TypeByName(typeName)
if schemaType == nil {
panic(fmt.Sprintf("bindnode utils: schema for [%T] does not contain that named type [%s]", ptrType, typ.Name()))
}
prototype[typ] = prototypeData{
bindnode.Prototype(ptrType, schemaType, options...),
options,
}
}

// TypeFromReader deserializes DAG-CBOR from a Reader and instantiates the Go
// type that's provided as a pointer via the ptrValue argument.
func TypeFromReader(r io.Reader, ptrValue interface{}) (interface{}, error) {
protoData := prototypeDataFor(ptrValue)
node, err := ipld.DecodeStreamingUsingPrototype(r, dagcbor.Decode, protoData.proto)
if err != nil {
return nil, err
}
typ := bindnode.Unwrap(node)
return typ, nil
}

// TypeFromNode converts an datamodel.Node into an appropriate Go type that's
// provided as a pointer via the ptrValue argument
func TypeFromNode(node datamodel.Node, ptrValue interface{}) (interface{}, error) {
protoData := prototypeDataFor(ptrValue)
if tn, ok := node.(schema.TypedNode); ok {
node = tn.Representation()
}
builder := protoData.proto.Representation().NewBuilder()
err := builder.AssignNode(node)
if err != nil {
return nil, err
}
typ := bindnode.Unwrap(builder.Build())
return typ, nil
}

// TypeToNode converts a Go type that's provided as a pointer via the ptrValue
// argument to an schema.TypedNode.
func TypeToNode(ptrValue interface{}) schema.TypedNode {
protoData := prototypeDataFor(ptrValue)
return bindnode.Wrap(ptrValue, protoData.proto.Type(), protoData.options...)
}

// TypeToWriter is a utility method that serializes a Go type that's provided as a
// pointer via the ptrValue argument as DAG-CBOR to a Writer
func TypeToWriter(ptrValue interface{}, w io.Writer) error {
return ipld.EncodeStreaming(w, TypeToNode(ptrValue), dagcbor.Encode)
}
165 changes: 165 additions & 0 deletions bindnodeutils/converters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package bindnodeutils

import (
"bytes"
"fmt"
"io"

"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/schema"
cbg "github.com/whyrusleeping/cbor-gen"

"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/crypto"
)

// go type converter functions for bindnode for common Filecoin data types

// CborGenCompatibleNodeBindnodeOption converts a CborGenCompatibleNode type to
// and from an Any field in a schema
var CborGenCompatibleNodeBindnodeOption = bindnode.TypedAnyConverter(&CborGenCompatibleNode{}, cborGenCompatibleNodeFromAny, cborGenCompatibleNodeToAny)

// BigIntBindnodeOption converts a big.Int type to and from a Bytes field in a
// schema
var BigIntBindnodeOption = bindnode.TypedBytesConverter(&big.Int{}, bigIntFromBytes, bigIntToBytes)

// TokenAmountBindnodeOption converts a filecoin abi.TokenAmount type to and
// from a Bytes field in a schema
var TokenAmountBindnodeOption = bindnode.TypedBytesConverter(&abi.TokenAmount{}, tokenAmountFromBytes, tokenAmountToBytes)

// AddressBindnodeOption converts a filecoin Address type to and from a Bytes
// field in a schema
var AddressBindnodeOption = bindnode.TypedBytesConverter(&address.Address{}, addressFromBytes, addressToBytes)

// SignatureBindnodeOption converts a filecoin Signature type to and from a
// Bytes field in a schema
var SignatureBindnodeOption = bindnode.TypedBytesConverter(&crypto.Signature{}, signatureFromBytes, signatureToBytes)

// CborGenCompatibleNode is for cbor-gen / go-ipld-prime compatibility, to
// replace Deferred types that are used to represent datamodel.Nodes.
// This shouldn't be used as a pointer (nullable/optional) as it can consume
// "Null" tokens and therefore be a Null. Instead, use
// CborGenCompatibleNode#IsNull to check for null status.
type CborGenCompatibleNode struct {
Node datamodel.Node
}

func (sn CborGenCompatibleNode) IsNull() bool {
return sn.Node == nil || sn.Node == datamodel.Null
}

// UnmarshalCBOR is for cbor-gen compatibility
func (sn *CborGenCompatibleNode) UnmarshalCBOR(r io.Reader) error {
// use cbg.Deferred.UnmarshalCBOR to figure out how much to pull
def := cbg.Deferred{}
if err := def.UnmarshalCBOR(r); err != nil {
return err
}
// convert it to a Node
na := basicnode.Prototype.Any.NewBuilder()
if err := dagcbor.Decode(na, bytes.NewReader(def.Raw)); err != nil {
return err
}
sn.Node = na.Build()
return nil
}

// MarshalCBOR is for cbor-gen compatibility
func (sn *CborGenCompatibleNode) MarshalCBOR(w io.Writer) error {
node := datamodel.Null
if sn != nil && sn.Node != nil {
node = sn.Node
if tn, ok := node.(schema.TypedNode); ok {
node = tn.Representation()
}
}
return dagcbor.Encode(node, w)
}

func cborGenCompatibleNodeFromAny(node datamodel.Node) (interface{}, error) {
return &CborGenCompatibleNode{Node: node}, nil
}

func cborGenCompatibleNodeToAny(iface interface{}) (datamodel.Node, error) {
sn, ok := iface.(*CborGenCompatibleNode)
if !ok {
return nil, fmt.Errorf("expected *CborGenCompatibleNode value")
}
if sn.Node == nil {
return datamodel.Null, nil
}
return sn.Node, nil
}

func tokenAmountFromBytes(b []byte) (interface{}, error) {
return bigIntFromBytes(b)
}

func bigIntFromBytes(b []byte) (interface{}, error) {
if len(b) == 0 {
return big.NewInt(0), nil
}
return big.FromBytes(b)
}

func tokenAmountToBytes(iface interface{}) ([]byte, error) {
return bigIntToBytes(iface)
}

func bigIntToBytes(iface interface{}) ([]byte, error) {
bi, ok := iface.(*big.Int)
if !ok {
return nil, fmt.Errorf("expected *big.Int value")
}
if bi == nil || bi.Int == nil {
*bi = big.Zero()
}
return bi.Bytes()
}

func addressFromBytes(b []byte) (interface{}, error) {
return address.NewFromBytes(b)
}

func addressToBytes(iface interface{}) ([]byte, error) {
addr, ok := iface.(*address.Address)
if !ok {
return nil, fmt.Errorf("expected *Address value")
}
return addr.Bytes(), nil
}

// Signature is a byteprefix union
func signatureFromBytes(b []byte) (interface{}, error) {
if len(b) > crypto.SignatureMaxLength {
return nil, fmt.Errorf("string too long")
}
if len(b) == 0 {
return nil, fmt.Errorf("string empty")
}
var s crypto.Signature
switch crypto.SigType(b[0]) {
default:
return nil, fmt.Errorf("invalid signature type in cbor input: %d", b[0])
case crypto.SigTypeSecp256k1:
s.Type = crypto.SigTypeSecp256k1
case crypto.SigTypeBLS:
s.Type = crypto.SigTypeBLS
}
s.Data = b[1:]
return &s, nil
}

func signatureToBytes(iface interface{}) ([]byte, error) {
s, ok := iface.(*crypto.Signature)
if !ok {
return nil, fmt.Errorf("expected *Signature value")
}
ba := append([]byte{byte(s.Type)}, s.Data...)
return ba, nil
}
21 changes: 8 additions & 13 deletions retrievalmarket/impl/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-statemachine/fsm"

"github.com/filecoin-project/go-fil-markets/bindnodeutils"
"github.com/filecoin-project/go-fil-markets/discovery"
"github.com/filecoin-project/go-fil-markets/retrievalmarket"
"github.com/filecoin-project/go-fil-markets/retrievalmarket/impl/clientstates"
Expand Down Expand Up @@ -108,17 +109,17 @@ func NewClient(
if err != nil {
return nil, err
}
err = dataTransfer.RegisterVoucherType((*retrievalmarket.DealProposal)(nil).Type(), nil)
err = dataTransfer.RegisterVoucherType(retrievalmarket.DealProposalType, nil)
if err != nil {
return nil, err
}
err = dataTransfer.RegisterVoucherType((*retrievalmarket.DealPayment)(nil).Type(), nil)
err = dataTransfer.RegisterVoucherType(retrievalmarket.DealPaymentType, nil)
if err != nil {
return nil, err
}
dataTransfer.SubscribeToEvents(dtutils.ClientDataTransferSubscriber(c.stateMachines))
transportConfigurer := dtutils.TransportConfigurer(network.ID(), &clientStoreGetter{c})
err = dataTransfer.RegisterTransportConfigurer((*retrievalmarket.DealProposal)(nil).Type(), transportConfigurer)
err = dataTransfer.RegisterTransportConfigurer(retrievalmarket.DealProposalType, transportConfigurer)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -404,19 +405,13 @@ func (c *clientDealEnvironment) OpenDataTransfer(ctx context.Context, to peer.ID
if proposal.SelectorSpecified() {
sel = proposal.Selector.Node
}
vouch, err := shared.TypeToNode(proposal)
if err != nil {
return datatransfer.ChannelID{}, err
}
return c.c.dataTransfer.OpenPullDataChannel(ctx, to, datatransfer.TypedVoucher{Voucher: vouch, Type: proposal.Type()}, proposal.PayloadCID, sel)
vouch := bindnodeutils.TypeToNode(proposal)
return c.c.dataTransfer.OpenPullDataChannel(ctx, to, datatransfer.TypedVoucher{Voucher: vouch, Type: retrievalmarket.DealProposalType}, proposal.PayloadCID, sel)
}

func (c *clientDealEnvironment) SendDataTransferVoucher(ctx context.Context, channelID datatransfer.ChannelID, payment *retrievalmarket.DealPayment) error {
vouch, err := shared.TypeToNode(payment)
if err != nil {
return err
}
return c.c.dataTransfer.SendVoucher(ctx, channelID, datatransfer.TypedVoucher{Voucher: vouch, Type: payment.Type()})
vouch := bindnodeutils.TypeToNode(payment)
return c.c.dataTransfer.SendVoucher(ctx, channelID, datatransfer.TypedVoucher{Voucher: vouch, Type: retrievalmarket.DealPaymentType})
}

func (c *clientDealEnvironment) CloseDataTransfer(ctx context.Context, channelID datatransfer.ChannelID) error {
Expand Down
14 changes: 7 additions & 7 deletions retrievalmarket/impl/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ import (
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"

"github.com/filecoin-project/go-fil-markets/bindnodeutils"
"github.com/filecoin-project/go-fil-markets/retrievalmarket"
retrievalimpl "github.com/filecoin-project/go-fil-markets/retrievalmarket/impl"
"github.com/filecoin-project/go-fil-markets/retrievalmarket/impl/testnodes"
"github.com/filecoin-project/go-fil-markets/retrievalmarket/migrations/maptypes"
"github.com/filecoin-project/go-fil-markets/retrievalmarket/network"
rmnet "github.com/filecoin-project/go-fil-markets/retrievalmarket/network"
"github.com/filecoin-project/go-fil-markets/shared"
"github.com/filecoin-project/go-fil-markets/shared_testutil"
tut "github.com/filecoin-project/go-fil-markets/shared_testutil"
)
Expand All @@ -47,10 +47,10 @@ func TestClient_Construction(t *testing.T) {

require.Len(t, dt.Subscribers, 1)
require.Len(t, dt.RegisteredVoucherTypes, 2)
require.Equal(t, dt.RegisteredVoucherTypes[0].VoucherType, (*retrievalmarket.DealProposal)(nil).Type())
require.Equal(t, dt.RegisteredVoucherTypes[1].VoucherType, (*retrievalmarket.DealPayment)(nil).Type())
require.Equal(t, dt.RegisteredVoucherTypes[0].VoucherType, retrievalmarket.DealProposalType)
require.Equal(t, dt.RegisteredVoucherTypes[1].VoucherType, retrievalmarket.DealPaymentType)
require.Len(t, dt.RegisteredTransportConfigurers, 1)
require.Equal(t, dt.RegisteredTransportConfigurers[0].VoucherType, (*retrievalmarket.DealProposal)(nil).Type())
require.Equal(t, dt.RegisteredTransportConfigurers[0].VoucherType, retrievalmarket.DealProposalType)
}

func TestClient_Query(t *testing.T) {
Expand Down Expand Up @@ -297,7 +297,7 @@ func TestClient_DuplicateRetrieve(t *testing.T) {

// Retrieve first payload CID from first peer
params := retrievalmarket.Params{
Selector: shared.CborGenCompatibleNode{},
Selector: bindnodeutils.CborGenCompatibleNode{},
PieceCID: &tut.GenerateCids(1)[0],
PricePerByte: abi.NewTokenAmount(1),
PaymentInterval: 1,
Expand Down Expand Up @@ -414,7 +414,7 @@ func TestMigrations(t *testing.T) {
PayloadCID: payloadCIDs[i],
ID: iDs[i],
Params: retrievalmarket.Params{
Selector: shared.CborGenCompatibleNode{
Selector: bindnodeutils.CborGenCompatibleNode{
Node: selectorparse.CommonSelector_ExploreAllRecursively,
},
PieceCID: pieceCIDs[i],
Expand Down Expand Up @@ -466,7 +466,7 @@ func TestMigrations(t *testing.T) {
PayloadCID: payloadCIDs[i],
ID: iDs[i],
Params: retrievalmarket.Params{
Selector: shared.CborGenCompatibleNode{
Selector: bindnodeutils.CborGenCompatibleNode{
Node: selectorparse.CommonSelector_ExploreAllRecursively,
},
PieceCID: pieceCIDs[i],
Expand Down
Loading

0 comments on commit 7765cd2

Please sign in to comment.