Skip to content

Commit

Permalink
Merge pull request #294 from okp4/feat/logic-crypto
Browse files Browse the repository at this point in the history
🧠 Logic: 🔑 `sha_hash/2` and `hex_bytes/2` predicates
  • Loading branch information
bdeneux authored Feb 21, 2023
2 parents 8ca4e15 + 1ac14dd commit 9a4ef44
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 0 deletions.
2 changes: 2 additions & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ var Registry = map[string]RegistryEntry{
"bank_spendable_balances/2": {predicate.BankSpendableBalances, 1},
"bank_locked_balances/2": {predicate.BankLockedBalances, 1},
"did_components/2": {predicate.DIDComponents, 1},
"sha_hash/2": {predicate.SHAHash, 1},
"hex_bytes/2": {predicate.HexBytes, 1},
}

// RegistryNames is the list of the predicate names in the Registry.
Expand Down
65 changes: 65 additions & 0 deletions x/logic/predicate/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package predicate

import (
"context"
"encoding/hex"
"fmt"

"github.com/ichiban/prolog/engine"
"github.com/okp4/okp4d/x/logic/util"
"github.com/tendermint/tendermint/crypto"
)

func SHAHash(vm *engine.VM, data, hash engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
var result []byte
switch d := env.Resolve(data).(type) {
case engine.Atom:
result = crypto.Sha256([]byte(d.String()))
return engine.Unify(vm, hash, BytesToList(result), cont, env)
default:
return engine.Error(fmt.Errorf("sha_hash/2: invalid data type: %T, should be Atom", d))
}
})
}

func HexBytes(vm *engine.VM, hexa, bts engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
var result []byte

switch h := env.Resolve(hexa).(type) {
case engine.Variable:
case engine.Atom:
src := []byte(h.String())
result = make([]byte, hex.DecodedLen(len(src)))
_, err := hex.Decode(result, src)
if err != nil {
return engine.Error(fmt.Errorf("hex_bytes/2: failed decode hexadecimal %w", err))
}
default:
return engine.Error(fmt.Errorf("hex_bytes/2: invalid hex type: %T, should be Atom or Variable", h))
}

switch b := env.Resolve(bts).(type) {
case engine.Variable:
if result == nil {
return engine.Error(fmt.Errorf("hex_bytes/2: nil hexadecimal conversion in input"))
}
return engine.Unify(vm, bts, BytesToList(result), cont, env)
case engine.Compound:
if b.Arity() != 2 || b.Functor().String() != "." {
return engine.Error(fmt.Errorf("hex_bytes/2: bytes should be a List, give %T", b))
}
iter := engine.ListIterator{List: b, Env: env}

src, err := ListToBytes(iter, env)
if err != nil {
return engine.Error(fmt.Errorf("hex_bytes/2: failed convert list into bytes: %w", err))
}
dst := hex.EncodeToString(src)
return engine.Unify(vm, hexa, util.StringToTerm(dst), cont, env)
default:
return engine.Error(fmt.Errorf("hex_bytes/2: invalid hex type: %T, should be Variable or List", b))
}
})
}
169 changes: 169 additions & 0 deletions x/logic/predicate/crypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//nolint:gocognit,lll
package predicate

import (
"fmt"
"testing"

"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ichiban/prolog/engine"
"github.com/okp4/okp4d/x/logic/testutil"
"github.com/okp4/okp4d/x/logic/types"
. "github.com/smartystreets/goconvey/convey"
"github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmdb "github.com/tendermint/tm-db"
)

func TestCryptoHash(t *testing.T) {
Convey("Given a test cases", t, func() {
cases := []struct {
program string
query string
wantResult []types.TermResults
wantError error
wantSuccess bool
}{
{
query: `sha_hash('foo', Hash).`,
wantResult: []types.TermResults{{
"Hash": "[44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]",
}},
wantSuccess: true,
},
{
query: `sha_hash(Foo, Hash).`,
wantResult: []types.TermResults{},
wantError: fmt.Errorf("sha_hash/2: invalid data type: engine.Variable, should be Atom"),
wantSuccess: false,
},
{
query: `sha_hash('bar',
[44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`,
wantSuccess: false,
},
{
query: `sha_hash('bar',
[252,222,43,46,219,165,107,244,8,96,31,183,33,254,155,92,51,141,16,238,66,158,160,79,174,85,17,182,143,191,143,185]).`,
wantResult: []types.TermResults{{}},
wantSuccess: true,
},
{
query: `sha_hash('bar',
[345,222,43,46,219,165,107,244,8,96,31,183,33,254,155,92,51,141,16,238,66,158,160,79,174,85,17,182,143,191,143,185]).`,
wantSuccess: false,
},
{
program: `test :- sha_hash('bar', H),
H == [252,222,43,46,219,165,107,244,8,96,31,183,33,254,155,92,51,141,16,238,66,158,160,79,174,85,17,182,143,191,143,185].`,
query: `test.`,
wantResult: []types.TermResults{{}},
wantSuccess: true,
},
{
program: `test :- sha_hash('bar', H),
H == [2252,222,43,46,219,165,107,244,8,96,31,183,33,254,155,92,51,141,16,238,66,158,160,79,174,85,17,182,143,191,143,185].`,
query: `test.`,
wantSuccess: false,
},
{
query: `hex_bytes(Hex,
[44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`,
wantResult: []types.TermResults{{"Hex": "'2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae'"}},
wantSuccess: true,
},
{
query: `hex_bytes('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae', Bytes).`,
wantResult: []types.TermResults{{
"Bytes": "[44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]",
}},
wantSuccess: true,
},
{
query: `hex_bytes('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae',
[44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`,
wantResult: []types.TermResults{{}},
wantSuccess: true,
},
{
query: `hex_bytes('3c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae',
[44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`,
wantSuccess: false,
},
{
query: `hex_bytes('fail',
[44,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`,
wantError: fmt.Errorf("hex_bytes/2: failed decode hexadecimal encoding/hex: invalid byte: U+0069 'i'"),
wantSuccess: false,
},
{
query: `hex_bytes('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae',
[345,38,180,107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`,
wantSuccess: false,
},
{
query: `hex_bytes('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae',
[345,38,'hey',107,104,255,198,143,249,155,69,60,29,48,65,52,19,66,45,112,100,131,191,160,249,138,94,136,98,102,231,174]).`,
wantSuccess: false,
wantError: fmt.Errorf("hex_bytes/2: failed convert list into bytes: invalid term type in list engine.Atom, only integer allowed"),
},
}
for nc, tc := range cases {
Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() {
Convey("and a context", func() {
db := tmdb.NewMemDB()
stateStore := store.NewCommitMultiStore(db)
ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger())

Convey("and a vm", func() {
interpreter := testutil.NewInterpreterMust(ctx)
interpreter.Register2(engine.NewAtom("sha_hash"), SHAHash)
interpreter.Register2(engine.NewAtom("hex_bytes"), HexBytes)

err := interpreter.Compile(ctx, tc.program)
So(err, ShouldBeNil)

Convey("When the predicate is called", func() {
sols, err := interpreter.QueryContext(ctx, tc.query)

Convey("Then the error should be nil", func() {
So(err, ShouldBeNil)
So(sols, ShouldNotBeNil)

Convey("and the bindings should be as expected", func() {
var got []types.TermResults
for sols.Next() {
m := types.TermResults{}
err := sols.Scan(m)
So(err, ShouldBeNil)

got = append(got, m)
}
if tc.wantError != nil {
So(sols.Err(), ShouldNotBeNil)
So(sols.Err().Error(), ShouldEqual, tc.wantError.Error())
} else {
So(sols.Err(), ShouldBeNil)

if tc.wantSuccess {
So(len(got), ShouldBeGreaterThan, 0)
So(len(got), ShouldEqual, len(tc.wantResult))
for iGot, resultGot := range got {
for varGot, termGot := range resultGot {
So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.wantResult[iGot][varGot])
}
}
} else {
So(len(got), ShouldEqual, 0)
}
}
})
})
})
})
})
})
}
})
}
23 changes: 23 additions & 0 deletions x/logic/predicate/util.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package predicate

import (
"fmt"
"sort"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -52,3 +53,25 @@ func CoinsToTerm(coins sdk.Coins) engine.Term {
func Tuple(args ...engine.Term) engine.Term {
return engine.Atom(0).Apply(args...)
}

func BytesToList(bt []byte) engine.Term {
terms := make([]engine.Term, 0, len(bt))
for _, b := range bt {
terms = append(terms, engine.Integer(b))
}
return engine.List(terms...)
}

func ListToBytes(terms engine.ListIterator, env *engine.Env) ([]byte, error) {
bt := make([]byte, 0)
for terms.Next() {
term := env.Resolve(terms.Current())
switch t := term.(type) {
case engine.Integer:
bt = append(bt, byte(t))
default:
return nil, fmt.Errorf("invalid term type in list %T, only integer allowed", term)
}
}
return bt, nil
}

0 comments on commit 9a4ef44

Please sign in to comment.