Skip to content

Commit

Permalink
Merge pull request #297 from okp4/feat/logic-bech32
Browse files Browse the repository at this point in the history
🧠 Logic: 📪 `bech32_address/2` predicate
  • Loading branch information
bdeneux authored Feb 28, 2023
2 parents 04739f2 + c31ba77 commit 7330922
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 0 deletions.
1 change: 1 addition & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ var Registry = map[string]RegistryEntry{
"did_components/2": {predicate.DIDComponents, 1},
"sha_hash/2": {predicate.SHAHash, 1},
"hex_bytes/2": {predicate.HexBytes, 1},
"bech32_address/2": {predicate.Bech32Address, 1},
}

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

import (
"context"
"fmt"

bech322 "github.com/cosmos/cosmos-sdk/types/bech32"
"github.com/ichiban/prolog/engine"
"github.com/okp4/okp4d/x/logic/util"
)

// Bech32Address is a predicate that convert a bech32 encoded string into base64 bytes and give the address prefix, or
// convert a prefix (HRP) and base64 encoded bytes to bech32 encoded string. The signature is as follows:
//
// bech32_address(-Address, +Bech32)
// bech32_address(+Address, -Bech32)
// bech32_address(+Address, +Bech32)
//
// where:
// - Address is a pair of, HRP (Human-Readable Part) containing the address prefix and the list of integers
// between 0 and 255 (byte) that represent the base64 encoded bech32 address string. Represented like this : -(HRP, Address)
// - Bech32 is an Atom or string representing the bech32 encoded string address
//
// # Example:
//
// - Convert the given bech32 address into base64 encoded byte by unify the prefix of given address (Hrp) and
// the base64 encoded value (Address).
//
// bech32_address(-(Hrp, Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').
//
// - Convert the given pair of HRP and base64 encoded address byte by unify the Bech32 string encoded value.
//
// bech32_address(-('okp4', [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).
func Bech32Address(vm *engine.VM, address, bech32 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
switch b := env.Resolve(bech32).(type) {
case engine.Variable:
case engine.Atom:
h, a, err := bech322.DecodeAndConvert(b.String())
if err != nil {
return engine.Error(fmt.Errorf("bech32_address/2: failed to decode Bech32: %w", err))
}
pair := AtomPair.Apply(util.StringToTerm(h), BytesToList(a))
return engine.Unify(vm, address, pair, cont, env)
default:
return engine.Error(fmt.Errorf("bech32_address/2: invalid Bech32 type: %T, should be Atom or Variable", b))
}

switch addressPair := env.Resolve(address).(type) {
case engine.Compound:
bech32Decoded, err := addressPairToBech32(addressPair, env)
if err != nil {
return engine.Error(fmt.Errorf("bech32_address/2: %w", err))
}
return engine.Unify(vm, bech32, util.StringToTerm(bech32Decoded), cont, env)
default:
return engine.Error(fmt.Errorf("bech32_address/2: invalid address type: %T, should be Compound (Hrp, Address)", addressPair))
}
})
}

func addressPairToBech32(addressPair engine.Compound, env *engine.Env) (string, error) {
if addressPair.Functor() != AtomPair || addressPair.Arity() != 2 {
return "", fmt.Errorf("address should be a Pair '-(Hrp, Address)'")
}

switch a := env.Resolve(addressPair.Arg(1)).(type) {
case engine.Compound:
if a.Arity() != 2 || a.Functor().String() != "." {
return "", fmt.Errorf("address should be a List of bytes")
}

iter := engine.ListIterator{List: a, Env: env}
data, err := ListToBytes(iter, env)
if err != nil {
return "", fmt.Errorf("failed to convert term to bytes list: %w", err)
}
hrp, ok := env.Resolve(addressPair.Arg(0)).(engine.Atom)
if !ok {
return "", fmt.Errorf("HRP should be instantiated")
}
b, err := bech322.ConvertAndEncode(hrp.String(), data)
if err != nil {
return "", fmt.Errorf("failed to convert base64 encoded address to bech32 string encoded: %w", err)
}

return b, nil
default:
return "", fmt.Errorf("address should be a Pair with a List of bytes in arity 2, give %T", addressPair.Arg(1))
}
}
168 changes: 168 additions & 0 deletions x/logic/predicate/address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//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 TestBech32(t *testing.T) {
Convey("Given a test cases", t, func() {
cases := []struct {
program string
query string
wantResult []types.TermResults
wantError error
wantSuccess bool
}{
{
query: `bech32_address(-(Hrp, Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{
"Hrp": "okp4",
"Address": "[163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]",
}},
wantSuccess: true,
},
{
query: `bech32_address(Address, 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{
"Address": "okp4-[163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]",
}},
wantSuccess: true,
},
{
query: `bech32_address(-('okp4', [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), foo(bar)).`,
wantError: fmt.Errorf("bech32_address/2: invalid Bech32 type: *engine.compound, should be Atom or Variable"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{
"Address": "[163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]",
}},
wantSuccess: true,
},
{
query: `bech32_address(-('okp5', Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantResult: []types.TermResults{{
"Bech32": "okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn",
}},
wantSuccess: true,
},
{
query: `bech32_address(-('okp4', [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{}},
wantSuccess: true,
},
{
query: `bech32_address(-(Hrp, [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{"Hrp": "okp4"}},
wantSuccess: true,
},
{
query: `bech32_address(-(Hrp, [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{"Hrp": "okp4"}},
wantSuccess: true,
},
{
query: `bech32_address(foo(Bar), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: address should be a Pair '-(Hrp, Address)'"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', ['8956',167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: invalid term type in list engine.Atom, only integer allowed"),
wantSuccess: false,
},
{
query: `bech32_address(-(Hrp, [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: HRP should be instantiated"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', hey(2)), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: address should be a List of bytes"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', 'foo'), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: address should be a Pair with a List of bytes in arity 2, give engine.Variable"),
wantSuccess: false,
},
{
query: `bech32_address(Address, Bech32).`,
wantError: fmt.Errorf("bech32_address/2: invalid address type: engine.Variable, should be Compound (Hrp, Address)"),
wantSuccess: false,
},
}
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("bech32_address"), Bech32Address)

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)
}
}
})
})
})
})
})
})
}
})
}

0 comments on commit 7330922

Please sign in to comment.