Skip to content

Commit

Permalink
Publish IPLD node built with go-ipld-prime.
Browse files Browse the repository at this point in the history
The RPC client still requires the legacy format,
but now I'm ready when the client is. Also, now
we can start to build linked data in SPACE™
  • Loading branch information
bahner committed Apr 23, 2024
1 parent 4aa7801 commit 1f8ffeb
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 55 deletions.
2 changes: 2 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# TODO

Write better documentation and maybe some examples. It's about time

Fix setting loglevel early with generation
3 changes: 3 additions & 0 deletions did/doc/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
log "github.com/sirupsen/logrus"
)

// How many fields to add to the node
const NUM_NODE_FIELDS = 7

type Document struct {
Context []string `cbor:"@context,toarray" json:"@context"`
ID string `cbor:"id" json:"id"`
Expand Down
2 changes: 1 addition & 1 deletion did/doc/keyset.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func NewFromKeyset(k set.Keyset) (*Document, error) {

func NewFromKeysetWithController(k set.Keyset, controller did.DID) (*Document, error) {

id := k.DID.IPNS
id := k.DID.Id

d, err := New(k.DID, controller)
if err != nil {
Expand Down
127 changes: 111 additions & 16 deletions did/doc/node.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,130 @@
package doc

import (
"bytes"
"fmt"

blocks "github.com/ipfs/go-block-format"
"github.com/ipfs/go-cid"
ipldcbor "github.com/ipfs/go-ipld-cbor"
format "github.com/ipfs/go-ipld-format"
mc "github.com/multiformats/go-multicodec"
mh "github.com/multiformats/go-multihash"
ipldlegacy "github.com/ipfs/go-ipld-legacy"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/node/basicnode"
multihash "github.com/multiformats/go-multihash"
)

func (d *Document) Node() (format.Node, cid.Cid, error) {
type documentNode struct {
Node format.Node
Cid cid.Cid
}

func (d *Document) Node() (documentNode, error) {

// Convert your struct to an IPLD node
node, err := d.ipldStructure()
if err != nil {
panic(err)
}

var buf []byte
buf, err = encodeIPLDNodeToDAGCBOR(node)
if err != nil {
return documentNode{}, fmt.Errorf("error encoding node to DAG-CBOR: %w", err)
}
// Create a CID for the block
c, err := cid.V1Builder{Codec: cid.DagCBOR, MhType: multihash.SHA2_256}.Sum(buf)
if err != nil {
return documentNode{}, fmt.Errorf("error creating CID: %w", err)
}

// Create the block
blk, err := blocks.NewBlockWithCid(buf, c)
if err != nil {
return documentNode{}, fmt.Errorf("error creating block: %w", err)
}

legacyNode := ipldlegacy.LegacyNode{Node: node, Block: blk}

n := documentNode{Node: &legacyNode, Cid: c}

return n, nil
}

func (d *Document) ipldStructure() (ipld.Node, error) {
nb := basicnode.Prototype.Map.NewBuilder()
ma, err := nb.BeginMap(7)
if err != nil {
return nil, err
}

// Context
contextNode, err := buildStringListNode(d.Context)
if err != nil {
return nil, err
}
ma.AssembleKey().AssignString("context")
ma.AssembleValue().AssignNode(contextNode)

// ID
ma.AssembleKey().AssignString("id")
ma.AssembleValue().AssignString(d.ID)

// Controllers
controllerNode, err := buildStringListNode(d.Controller)
if err != nil {
return nil, err
}
ma.AssembleKey().AssignString("controller")
ma.AssembleValue().AssignNode(controllerNode)

// Hash the CBOR data to create a CID
data, err := d.MarshalToCBOR()
// VerificationMethod
ma.AssembleKey().AssignString("verificationMethod")
verificationMethodsNode, err := buildVerificationMethodList(d.VerificationMethod)
if err != nil {
return nil, cid.Cid{}, err
return nil, err
}
ma.AssembleValue().AssignNode(verificationMethodsNode)

// AssertionMethod
ma.AssembleKey().AssignString("assertionMethod")
ma.AssembleValue().AssignString(d.AssertionMethod)

// KeyAgreement
ma.AssembleKey().AssignString("keyAgreement")
ma.AssembleValue().AssignString(d.KeyAgreement)

hash, err := mh.Sum(data, mh.SHA2_256, -1)
// Proof
proofNode, err := buildProofNode(d.Proof)
if err != nil {
return nil, cid.Cid{}, err
return nil, err
}
ma.AssembleKey().AssignString("proof")
ma.AssembleValue().AssignNode(proofNode)

// Create a CID with the DagCBOR codec
codecType := uint64(mc.DagCbor)
c := cid.NewCidV1(codecType, hash)
ma.Finish()

return nb.Build(), nil
}

// Use go-ipld-cbor to decode the CBOR data into an IPLD node
node, err := ipldcbor.Decode(data, mh.SHA2_256, -1)
func buildStringListNode(controllers []string) (ipld.Node, error) {
nb := basicnode.Prototype.List.NewBuilder()
la, err := nb.BeginList(-1)
if err != nil {
return nil, cid.Cid{}, err
return nil, err
}
for _, controller := range controllers {
la.AssembleValue().AssignString(controller)
}
la.Finish()

return nb.Build(), nil
}

return node, c, nil
func encodeIPLDNodeToDAGCBOR(node ipld.Node) ([]byte, error) {
var buf bytes.Buffer
if err := dagcbor.Encode(node, &buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
31 changes: 29 additions & 2 deletions did/doc/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"fmt"

"github.com/bahner/go-ma/key"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/multiformats/go-multibase"
)

const (
proofType = "MultiformatSignature2023"
proofPurpose = "assertionMethod"
proofType = "MultiformatSignature2023"
proofPurpose = "assertionMethod"
proofNumFields = 4
)

type Proof struct {
Expand Down Expand Up @@ -56,3 +59,27 @@ func NewProof(proofValue string, vm string) Proof {
VerificationMethod: vm,
}
}

func buildProofNode(proof Proof) (ipld.Node, error) {
nb := basicnode.Prototype.Map.NewBuilder()
ma, err := nb.BeginMap(proofNumFields)
if err != nil {
return nil, err
}

ma.AssembleKey().AssignString("type")
ma.AssembleValue().AssignString(proof.Type)

ma.AssembleKey().AssignString("verificationMethod")
ma.AssembleValue().AssignString(proof.VerificationMethod)

ma.AssembleKey().AssignString("proofPurpose")
ma.AssembleValue().AssignString(proof.ProofPurpose)

ma.AssembleKey().AssignString("proofValue")
ma.AssembleValue().AssignString(proof.ProofValue)

ma.Finish()

return nb.Build(), nil
}
77 changes: 45 additions & 32 deletions did/doc/publication.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,45 @@ import (

var ErrAlreadyPublished = errors.New("Document is already published")

type PublishOptions struct {
Ctx context.Context
Pin bool
Force bool
AllowBigBlock bool
}

func DefaultPublishOptions() *PublishOptions {
return &PublishOptions{
Ctx: context.Background(),
Pin: true,
Force: true,
AllowBigBlock: false,
}
}

// Publishes document to a key known by the IPFS node. This maybe a peer ID or a name.
// Both provided as a simple string.
// The only option we honour is the force option. If set to true we will update the existing pin regardless.
// Publishes the document to IPFS and IPNS. This is a little noise, as it does a lot of things.
// Notably it can take a long time, which is why there's also a gorutine version.
func (d *Document) Publish() (ipns.Name, error) {

ctx := context.Background()
alreadyPublishedString := "'to' cid was already recursively pinned"

node, newCID, err := d.Node()
node, err := d.Node()
if err != nil {
return ipns.Name{}, fmt.Errorf("DocPublish: %w", err)
}
newImmutablePath := path.FromCid(newCID)
newImmutablePath := path.FromCid(node.Cid)

// Get the IPFS API
a := api.GetIPFSAPI()
a.Dag().Add(ctx, node)

// If an existing document is already published and Pin is set we need to update the existing pin f asked to force.
err = a.Pin().Update(ctx, d.immutablePath, newImmutablePath)
// Add the Document Node to the IPFS DAG
err = a.Dag().Add(ctx, node.Node)
if err != nil {
return ipns.Name{}, fmt.Errorf("DocPublish: %w", err)
}
log.Infof("DocPublish: Successfully added document %s to IPLD", node.Cid.String())

// Pin the document
log.Infof("Pinning %s in IPFS. Please wait ...", newImmutablePath.String())
err = pinDocument(ctx, d.immutablePath, newImmutablePath)
if err != nil {
if err.Error() == alreadyPublishedString {
return ipns.Name{}, fmt.Errorf("DocPublish: %w", ErrAlreadyPublished)
}
return ipns.Name{}, fmt.Errorf("DocPublish: %w", err)
}

// Now that the document is pinned we can update the path to the new one.
d.immutablePath = newImmutablePath

log.Debugf("DocPublish: Announcing publication of document %s to IPNS. Please wait ...", newCID.String())
n, err := a.Name().Publish(ctx, newImmutablePath, options.Name.Key(d.did.IPNS))
log.Debugf("DocPublish: Announcing publication of document %s to IPNS. Please wait ...", node.Cid.String())
name, err := a.Name().Publish(ctx, newImmutablePath, options.Name.Key(d.did.IPNS))
if err != nil {
return ipns.Name{}, fmt.Errorf("DocPublish: %w", err)
}
log.Debugf("DocPublish: Successfully announced publication of document %s to %s", newCID.String(), n.AsPath().String())
return n, nil
log.Debugf("DocPublish: Successfully announced publication of document %s to %s", node.Cid.String(), name.AsPath().String())
return name, nil

}

Expand All @@ -85,6 +71,33 @@ func (d *Document) PublishGoroutine(wg *sync.WaitGroup, cancel context.CancelFun

}

func pinDocument(ctx context.Context, oldP path.ImmutablePath, newP path.ImmutablePath) error {

alreadyPublishedString := "'to' cid was already recursively pinned"

a := api.GetIPFSAPI()

var err error
if (oldP == path.ImmutablePath{}) {
err = a.Pin().Add(ctx, newP)
if err != nil {
return fmt.Errorf("DocPublish: %w", err)
}
} else {
// If an existing document is already published and Pin is set we need to update the existing pin f asked to force.
err = a.Pin().Update(ctx, oldP, newP)
if err != nil {
if err.Error() == alreadyPublishedString {
return fmt.Errorf("DocPublish: %w", ErrAlreadyPublished)
}
return fmt.Errorf("DocPublish: %w", err)
}
}

return nil

}

// Takes an IPFS path name and returns the root CID.
// The cached field tells the function whether to use the cached value or not.
func resolveRootCID(name string) (cid.Cid, error) {
Expand Down
51 changes: 51 additions & 0 deletions did/doc/verification_method.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import (
"fmt"

"github.com/bahner/go-ma/did"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/node/basicnode"
log "github.com/sirupsen/logrus"
)

const verificationMethodNumFields = 4

// VerificationMethod defines the structure of a Verification Method
type VerificationMethod struct {
// The full name of the verification method, eg. did:ma:123456789abcdefghi#signature-key-id
Expand Down Expand Up @@ -196,3 +200,50 @@ func (vm VerificationMethod) IsValid() bool {

return vm.Verify() == nil
}

func buildVerificationMethodNode(vm VerificationMethod) (ipld.Node, error) {
nb := basicnode.Prototype.Map.NewBuilder()
ma, err := nb.BeginMap(verificationMethodNumFields)
if err != nil {
return nil, err
}

ma.AssembleKey().AssignString("id")
ma.AssembleValue().AssignString(vm.ID)

ma.AssembleKey().AssignString("type")
ma.AssembleValue().AssignString(vm.Type)

controllerNode, err := buildStringListNode(vm.Controller)
if err != nil {
return nil, err
}
ma.AssembleKey().AssignString("controller")
ma.AssembleValue().AssignNode(controllerNode)

ma.AssembleKey().AssignString("publicKeyMultibase")
ma.AssembleValue().AssignString(vm.PublicKeyMultibase)

ma.Finish()

return nb.Build(), nil
}

func buildVerificationMethodList(vms []VerificationMethod) (ipld.Node, error) {
nb := basicnode.Prototype.List.NewBuilder()
la, err := nb.BeginList(-1)
if err != nil {
return nil, err
}

for _, vm := range vms {
vmNode, err := buildVerificationMethodNode(vm)
if err != nil {
return nil, err
}
la.AssembleValue().AssignNode(vmNode)
}
la.Finish()

return nb.Build(), nil
}
Loading

0 comments on commit 1f8ffeb

Please sign in to comment.