diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go new file mode 100644 index 0000000..8d8a64f --- /dev/null +++ b/toolkit/client/pool.go @@ -0,0 +1,128 @@ +package client + +import ( + "math" + "time" + + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +type Pool struct { + dlgs map[cid.Cid]*delegation.Bundle +} + +func NewPool() *Pool { + return &Pool{dlgs: make(map[cid.Cid]*delegation.Bundle)} +} + +func (p *Pool) AddBundle(bundle *delegation.Bundle) { + p.dlgs[bundle.Cid] = bundle +} + +// FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. +// - issuer: the DID of the client, also the issuer of the invocation token +// - audience: the DID of the resource to operate on, also the audience (or subject if defined) of the invocation token +// - cmd: the command to execute +// - args: the args to execute +// Note: the implemented algorithm won't perform well with a large number of delegations. +func (p *Pool) FindProof(iss did.DID, aud did.DID, cmd command.Command) []cid.Cid { + p.trim() + + // Find the possible leaf delegations, directly matching the invocation parameters + var candidateLeaf []*delegation.Bundle + + for _, bundle := range p.dlgs { + dlg := bundle.Decoded + + // The Subject of each delegation must equal the invocation's Audience field. - 4f + if dlg.Subject() != aud { + continue + } + // The first proof must be issued to the Invoker (audience DID). - 4c + // The Issuer of each delegation must be the Audience in the next one. - 4d + if dlg.Audience() != iss { + continue + } + // The command of each delegation must "allow" the one before it. - 4g + if !dlg.Command().Covers(cmd) { + continue + } + // Time bound - 3b, 3c + if !dlg.IsValidNow() { + continue + } + + // graph.WriteString("[*] --> " + bundle.Cid.String() + "\n") + candidateLeaf = append(candidateLeaf, bundle) + } + + type state struct { + bundle *delegation.Bundle + path []cid.Cid + size int + } + + var bestSize = math.MaxInt + var bestProof []cid.Cid + + // Perform a depth-first search on the DAG of connected delegations, for each of our candidates + for _, leaf := range candidateLeaf { + var stack = []state{{bundle: leaf, path: []cid.Cid{leaf.Cid}, size: len(leaf.Sealed)}} + + for len(stack) > 0 { + // dequeue a delegation + cur := stack[len(stack)-1] + stack = stack[:len(stack)-1] + at := cur.bundle + + // if it's a root delegation, we found a valid proof + if at.Decoded.Issuer() == at.Decoded.Subject() { + if len(bestProof) == 0 || len(cur.path) < len(bestProof) || len(cur.path) == len(bestProof) && cur.size < bestSize { + bestProof = append([]cid.Cid{}, cur.path...) // make a copy + bestSize = cur.size + continue + } + } + + // find parent delegation for our current delegation + for _, candidate := range p.dlgs { + // The Subject of each delegation must equal the invocation's Audience field. - 4f + if candidate.Decoded.Subject() != aud { + continue + } + // The first proof must be issued to the Invoker (audience DID). - 4c + // The Issuer of each delegation must be the Audience in the next one. - 4d + if candidate.Decoded.Audience() != at.Decoded.Issuer() { + continue + } + // The command of each delegation must "allow" the one before it. - 4g + if !candidate.Decoded.Command().Covers(at.Decoded.Command()) { + continue + } + // Time bound - 3b, 3c + if !candidate.Decoded.IsValidNow() { + continue + } + + newPath := append([]cid.Cid{}, cur.path...) // make copy + newPath = append(newPath, candidate.Cid) + stack = append(stack, state{bundle: candidate, path: newPath, size: cur.size + len(candidate.Sealed)}) + } + } + } + + return bestProof +} + +// trim removes expired tokens +func (p *Pool) trim() { + now := time.Now() + for c, bundle := range p.dlgs { + if bundle.Decoded.Expiration() != nil && bundle.Decoded.Expiration().Before(now) { + delete(p.dlgs, c) + } + } +} diff --git a/toolkit/client/pool_test.go b/toolkit/client/pool_test.go new file mode 100644 index 0000000..a8f756b --- /dev/null +++ b/toolkit/client/pool_test.go @@ -0,0 +1,29 @@ +package client + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did/didtest" + "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" +) + +func TestFindProof(t *testing.T) { + p := NewPool() + + for _, bundle := range delegationtest.AllBundles { + p.AddBundle(bundle) + } + + require.Equal(t, delegationtest.ProofAliceBob, + p.FindProof(didtest.PersonaBob.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarol, + p.FindProof(didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarolDan, + p.FindProof(didtest.PersonaDan.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, + p.FindProof(didtest.PersonaErin.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, + p.FindProof(didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + +}