Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧠 Logic: 📪 bech32_address/2 predicate #297

Merged
merged 9 commits into from
Feb 28, 2023
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 {
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
})
})
})
})
})
})
}
})
}